diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a2e01b9..128fd0222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,25 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## v0.23.0 +### Added +- Add **REoptLogger** type of global logger with a standard out to the console and to a dictionary + - Instantiate `logREopt` as the global logger in `__init()__` function call as a global variable + - Handle Warn or Error logs to save them along with information on where they occurred + - Try-catch `core/reopt.jl -> run_reopt()` functions. Process any errors when catching the error. + - Add Warnings and Errors from `logREopt` to results dictionary. If error is unhandled in REopt, include a stacktrace + - Add a `status` of `error` to results for consistency + - Ensure all error text is returned as strings for proper handling in the API +- Add `handle_errors(e::E, stacktrace::V) where {E <: Exception, V <: Vector}` and `handle_errors()` to `core/reopt.jl` to include info, warn and errors from REopt input data processing, optimization, and results processing in the returned dictionary. +- Tests for user-inputs of `ElectricTariff` `demand_lookback_months` and `demand_lookback_range` +### Changed +- `core/reopt.jl` added try-catch statements to call `handle_errors()` when there is a REopt error (handled or unhandled) and return it to the requestor/user. +### Fixed +- URDB lookback was not incorporated based on the descriptions of how the 3 lookback variables should be entered in the code. Modified `parse_urdb_lookback_charges` function to correct. +- TOU demand for 15-min load was only looking at the first 8760 timesteps. +- Tiered energy rates jsons generated by the webtool errored and could not run. +- Aligned lookback parameter names from URDB with API + # v0.22.0 ### Added - Simulated load function which mimicks the REopt_API /simulated_load endpoint for getting commercial reference building load data from annual or monthly energy data, or blended/hybrid buildings diff --git a/Manifest.toml b/Manifest.toml index 6f112877b..982ace98b 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,6 +1,6 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.7.2" +julia_version = "1.7.1" manifest_format = "2.0" [[deps.AbstractFFTs]] diff --git a/Project.toml b/Project.toml index de354bfd8..bcdd8a152 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 "] -version = "0.22.0" +version = "0.23.0" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" diff --git a/src/REopt.jl b/src/REopt.jl index 59ee5126e..e1b547d32 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -78,7 +78,7 @@ end const EXISTING_BOILER_EFFICIENCY = 0.8 const GAL_PER_M3 = 264.172 # [gal/m^3] -const KWH_PER_GAL_DIESEL = 40.7 # [kWh/gal_diesel] +const KWH_PER_GAL_DIESEL = 40.7 # [kWh/gal_diesel] higher heating value of diesel const KWH_PER_MMBTU = 293.07107 # [kWh/mmbtu] const KWH_THERMAL_PER_TONHOUR = 3.51685 const TONNE_PER_LB = 1/2204.62 # [tonne/lb] @@ -119,6 +119,8 @@ const FUEL_DEFAULTS = Dict( ) ) +include("logging.jl") + include("keys.jl") include("core/types.jl") include("core/utils.jl") diff --git a/src/constraints/battery_degradation.jl b/src/constraints/battery_degradation.jl index b1582d181..cf6ad573a 100644 --- a/src/constraints/battery_degradation.jl +++ b/src/constraints/battery_degradation.jl @@ -197,7 +197,7 @@ function add_degradation(m, p; b="ElectricStorage") # add augmentation cost to objective # maintenance_cost_per_kwh must have length == length(days) - 1, i.e. starts on day 2 else - @error "Battery maintenance strategy $strategy is not supported. Choose from augmentation and replacement." + throw(@error("Battery maintenance strategy $strategy is not supported. Choose from augmentation and replacement.")) end @objective(m, Min, m[:Costs] + m[:degr_cost]) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 75697a31d..c6942d542 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -72,8 +72,8 @@ function add_export_constraints(m, p; _n="") end else if !(isempty(_n)) - @error """Binaries decisions for net metering capacity limit is not implemented for multinode models to keep - them linear. Please set the net metering limit to zero or equal to the interconnection limit.""" + throw(@error("Binaries decisions for net metering capacity limit is not implemented for multinode models to keep + them linear. Please set the net metering limit to zero or equal to the interconnection limit.")) end binNEM = @variable(m, binary = true) @@ -298,25 +298,10 @@ function add_demand_lookback_constraints(m, p; _n="") if p.s.electric_tariff.demand_lookback_range != 0 # then the dvPeakDemandLookback varies by month ##Constraint (12e): dvPeakDemandLookback is the highest peak demand in DemandLookbackMonths - for mth in p.months - if mth > p.s.electric_tariff.demand_lookback_range - @constraint(m, [lm in 1:p.s.electric_tariff.demand_lookback_range, ts in p.s.electric_tariff.time_steps_monthly[mth - lm]], - m[Symbol(dv)][mth] ≥ sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] - for tier in 1:p.s.electric_tariff.n_energy_tiers ) - ) - else # need to handle rollover months - for lm in 1:p.s.electric_tariff.demand_lookback_range - lkbkmonth = mth - lm - if lkbkmonth ≤ 0 - lkbkmonth += 12 - end - @constraint(m, [ts in p.s.electric_tariff.time_steps_monthly[lkbkmonth]], - m[Symbol(dv)][mth] ≥ sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] - for tier in 1:p.s.electric_tariff.n_energy_tiers ) - ) - end - end - end + @constraint(m, [mth in p.months, lm in 1:p.s.electric_tariff.demand_lookback_range, ts in p.s.electric_tariff.time_steps_monthly[mod(mth - lm - 1, 12) + 1]], + m[Symbol(dv)][mth] ≥ sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] + for tier in 1:p.s.electric_tariff.n_energy_tiers ) + ) ##Constraint (12f): Ratchet peak demand charge is bounded below by lookback @constraint(m, [mth in p.months], @@ -326,7 +311,7 @@ function add_demand_lookback_constraints(m, p; _n="") else # dvPeakDemandLookback does not vary by month - ##Constraint (12e): dvPeakDemandLookback is the highest peak demand in DemandLookbackMonths + ##Constraint (12e): dvPeakDemandLookback is the highest peak demand in demand_lookback_months @constraint(m, [lm in p.s.electric_tariff.demand_lookback_months], m[Symbol(dv)][1] >= sum(m[Symbol("dvPeakDemandMonth"*_n)][lm, tier] for tier in 1:p.s.electric_tariff.n_monthly_demand_tiers) ) diff --git a/src/constraints/generator_constraints.jl b/src/constraints/generator_constraints.jl index be6e586b4..87431f7c7 100644 --- a/src/constraints/generator_constraints.jl +++ b/src/constraints/generator_constraints.jl @@ -45,7 +45,7 @@ function add_binGenIsOnInTS_constraints(m,p) @constraint(m, [t in p.techs.gen, ts in p.time_steps], m[:dvRatedProduction][t, ts] <= p.max_sizes[t] * m[:binGenIsOnInTS][t, ts] ) - # Note: min_turn_down_fraction is only enforced when off_grid_flag is true and in p.time_steps_with_grid, but not for grid outages for on-grid analyses + # Note: min_turn_down_fraction is only enforced when `off_grid_flag` is true and in p.time_steps_with_grid, but not for grid outages for on-grid analyses if p.s.settings.off_grid_flag @constraint(m, [t in p.techs.gen, ts in p.time_steps_without_grid], p.s.generator.min_turn_down_fraction * m[:dvSize][t] - m[:dvRatedProduction][t, ts] <= diff --git a/src/core/absorption_chiller.jl b/src/core/absorption_chiller.jl index 5aeaf758e..8af91f4b5 100644 --- a/src/core/absorption_chiller.jl +++ b/src/core/absorption_chiller.jl @@ -103,7 +103,7 @@ function AbsorptionChiller(d::Dict; if !isnothing(cooling_load) load_max_tons = maximum(cooling_load.loads_kw_thermal) / KWH_THERMAL_PER_TONHOUR else - throw(@error "Invalid argument cooling_load=nothing: a CoolingLoad is required for the AbsorptionChiller to be a technology option.") + throw(@error("Invalid argument cooling_load=nothing: a CoolingLoad is required for the AbsorptionChiller to be a technology option.")) end if !isnothing(existing_boiler) boiler_type = existing_boiler.production_type @@ -190,7 +190,7 @@ function get_absorption_chiller_defaults(; elseif chp_prime_mover in PRIME_MOVERS #if chp_prime mover is blank or is anything but "combustion_turbine" then assume hot water thermal_consumption_hot_water_or_steam = "hot_water" else - throw(@error "Invalid argument for `prime_mover`; must be in $PRIME_MOVERS") + throw(@error("Invalid argument for `prime_mover`; must be in $PRIME_MOVERS")) end elseif !isnothing(boiler_type) thermal_consumption_hot_water_or_steam = boiler_type @@ -200,7 +200,7 @@ function get_absorption_chiller_defaults(; end else if !(thermal_consumption_hot_water_or_steam in HOT_WATER_OR_STEAM) - throw(@error "Invalid argument for `thermal_consumption_hot_water_or_steam`; must be `hot_water` or `steam`") + throw(@error("Invalid argument for `thermal_consumption_hot_water_or_steam`; must be `hot_water` or `steam`")) end end diff --git a/src/core/chp.jl b/src/core/chp.jl index 5b5dc3e3d..4df3a2115 100644 --- a/src/core/chp.jl +++ b/src/core/chp.jl @@ -96,7 +96,7 @@ prime_movers = ["recip_engine", "micro_turbine", "combustion_turbine", "fuel_cel If no information is provided, the default `prime_mover` is `recip_engine` and the `size_class` is 1 which represents the widest range of sizes available. - `fuel_cost_per_mmbtu` is always required + `fuel_cost_per_mmbtu` is always required and can be a scalar, a list of 12 monthly values, or a time series of values for every time step """ Base.@kwdef mutable struct CHP <: AbstractCHP @@ -172,7 +172,7 @@ function CHP(d::Dict; # Check for required fuel cost if !haskey(d, "fuel_cost_per_mmbtu") - throw(@error "CHP must have the required fuel_cost_per_mmbtu input") + throw(@error("CHP must have the required fuel_cost_per_mmbtu input")) end # Create CHP struct from inputs, to be mutated as needed chp = CHP(; dictkeys_tosymbols(d)...) @@ -200,7 +200,7 @@ function CHP(d::Dict; @warn "Ignoring `chp.tech_sizes_for_cost_curve` input because `chp.installed_cost_per_kw` is a scalar" end elseif length(chp.installed_cost_per_kw) > 1 && length(chp.installed_cost_per_kw) != length(chp.tech_sizes_for_cost_curve) - throw(@error "To model CHP cost curve, you must provide `chp.tech_sizes_for_cost_curve` vector of equal length to `chp.installed_cost_per_kw`") + throw(@error("To model CHP cost curve, you must provide `chp.tech_sizes_for_cost_curve` vector of equal length to `chp.installed_cost_per_kw`")) elseif isempty(chp.tech_sizes_for_cost_curve) && isempty(chp.installed_cost_per_kw) update_installed_cost_params = true elseif isempty(chp.prime_mover) @@ -333,13 +333,13 @@ function get_chp_defaults_prime_mover_size_class(;hot_water_or_steam::Union{Stri # Inputs validation if !isnothing(prime_mover) if !(prime_mover in prime_movers) # Validate user-entered hot_water_or_steam - throw(@error "Invalid argument for `prime_mover`; must be in $prime_movers") + throw(@error("Invalid argument for `prime_mover`; must be in $prime_movers")) end end if !isnothing(hot_water_or_steam) # Option 1 if prime_mover also not input if !(hot_water_or_steam in ["hot_water", "steam"]) # Validate user-entered hot_water_or_steam - throw(@error "Invalid argument for `hot_water_or_steam``; must be `hot_water` or `steam`") + throw(@error("Invalid argument for `hot_water_or_steam``; must be `hot_water` or `steam`")) end else # Options 2, 3, or 4 hot_water_or_steam = "hot_water" @@ -347,14 +347,14 @@ function get_chp_defaults_prime_mover_size_class(;hot_water_or_steam::Union{Stri if !isnothing(avg_boiler_fuel_load_mmbtu_per_hour) # Option 1 if avg_boiler_fuel_load_mmbtu_per_hour <= 0 - throw(@error "avg_boiler_fuel_load_mmbtu_per_hour must be >= 0.0") + throw(@error("avg_boiler_fuel_load_mmbtu_per_hour must be >= 0.0")) end end if !isnothing(size_class) && !isnothing(prime_mover) # Option 3 n_classes = length(prime_mover_defaults_all[prime_mover]["installed_cost_per_kw"]) - if size_class < 1 || size_class >= n_classes - throw(@error "The size class input is outside the valid range of 1-$n_classes for prime_mover $prime_mover") + if size_class < 1 || size_class > n_classes + throw(@error("The size class $size_class input is outside the valid range of 1 to $n_classes for prime_mover $prime_mover")) end end @@ -397,8 +397,8 @@ function get_chp_defaults_prime_mover_size_class(;hot_water_or_steam::Union{Stri # If size class is specified use that and ignore heuristic CHP sizing for determining size class if !isnothing(size_class) - if size_class < 1 || size_class >= n_classes - throw(@error "The size class input is outside the valid range of 1-$n_classes for prime_mover $prime_mover") + if size_class < 1 || size_class > n_classes + throw(@error("The size class $size_class input is outside the valid range of 1 to $n_classes for prime_mover $prime_mover")) end # If size class is not specified, heuristic sizing based on avg thermal load and size class 0 efficiencies elseif isnothing(size_class) && !isnothing(chp_elec_size_heuristic_kw) diff --git a/src/core/cost_curve.jl b/src/core/cost_curve.jl index 7201294a9..46d913aec 100644 --- a/src/core/cost_curve.jl +++ b/src/core/cost_curve.jl @@ -332,7 +332,7 @@ function cost_curve(tech::AbstractTech, financial::Financial) for s in range(1, stop=n_segments) if cost_curve_bp_x[s + 1] <= 0 # Not sure how else to handle this case, perhaps there is a better way to handle it? - @error "Invalid cost curve for {$nameof(T)}. Value at index {$s} ({$cost_curve_bp_x[s + 1]}) cannot be less than or equal to 0." + throw(@error("Invalid cost curve for {$nameof(T)}. Value at index {$s} ({$cost_curve_bp_x[s + 1]}) cannot be less than or equal to 0.")) end # Remove federal incentives for ITC basis and tax benefit calculations diff --git a/src/core/doe_commercial_reference_building_loads.jl b/src/core/doe_commercial_reference_building_loads.jl index 604c3b368..cc8caab79 100644 --- a/src/core/doe_commercial_reference_building_loads.jl +++ b/src/core/doe_commercial_reference_building_loads.jl @@ -72,7 +72,7 @@ function find_ashrae_zone_city(lat, lon; get_zone=false) end end if isnothing(archgdal_city) - @info "Could not find latitude/longitude in U.S. Using geometrically nearest city." + @warn "Could not find latitude/longitude in U.S. Using geometrically nearest city." elseif !get_zone return archgdal_city end @@ -314,7 +314,7 @@ function get_monthly_energy(power_profile::AbstractArray{<:Real,1}; if !isempty(power_profile) monthly_energy_total[month] = sum(power_profile[t0:t0+plus_hours-1]) else - throw(@error "Must provide power_profile") + throw(@error("Must provide power_profile")) end t0 += plus_hours end diff --git a/src/core/electric_load.jl b/src/core/electric_load.jl index 4ae2ab5d4..3fdab5f9e 100644 --- a/src/core/electric_load.jl +++ b/src/core/electric_load.jl @@ -128,7 +128,7 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o ) if off_grid_flag && !(critical_load_fraction == 1.0) - @warn "ElectricLoad critical_load_fraction must be 1.0 (100%) for off-grid scenarios. Any other value will be overriden when off_grid_flag is True. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction." + @warn "ElectricLoad critical_load_fraction must be 1.0 (100%) for off-grid scenarios. Any other value will be overriden when `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction." critical_load_fraction = 1.0 end @@ -145,25 +145,24 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o if length(loads_kw) > 0 if !(length(loads_kw) / time_steps_per_hour ≈ 8760) - throw(@error "Provided electric load does not match the time_steps_per_hour.") + throw(@error("Provided electric load does not match the time_steps_per_hour.")) end elseif !isempty(path_to_csv) try loads_kw = vec(readdlm(path_to_csv, ',', Float64, '\n')) catch e - @error "Unable to read in electric load profile from $path_to_csv. Please provide a valid path to a csv with no header." - throw(e) + throw(@error("Unable to read in electric load profile from $path_to_csv. Please provide a valid path to a csv with no header.")) end if !(length(loads_kw) / time_steps_per_hour ≈ 8760) - throw(@error "Provided electric load does not match the time_steps_per_hour.") + throw(@error("Provided electric load does not match the time_steps_per_hour.")) end elseif !isempty(doe_reference_name) # NOTE: must use year that starts on Sunday with DOE reference doe_ref_profiles if year != 2017 - @debug "Changing load profile year to 2017 because DOE reference profiles start on a Sunday." + @warn "Changing load profile year to 2017 because DOE reference profiles start on a Sunday." end year = 2017 loads_kw = BuiltInElectricLoad(city, doe_reference_name, latitude, longitude, year, annual_kwh, monthly_totals_kwh) @@ -174,14 +173,14 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o blended_doe_reference_names, blended_doe_reference_percents, city, annual_kwh, monthly_totals_kwh) else - error("Cannot construct ElectricLoad. You must provide either [loads_kw], [doe_reference_name, city], + throw(@error("Cannot construct ElectricLoad. You must provide either [loads_kw], [doe_reference_name, city], [doe_reference_name, latitude, longitude], - or [blended_doe_reference_names, blended_doe_reference_percents] with city or latitude and longitude.") + or [blended_doe_reference_names, blended_doe_reference_percents] with city or latitude and longitude.")) end if length(loads_kw) < 8760*time_steps_per_hour loads_kw = repeat(loads_kw, inner=Int(time_steps_per_hour / (length(loads_kw)/8760))) - @info "Repeating electric loads in each hour to match the time_steps_per_hour." + @warn "Repeating electric loads in each hour to match the time_steps_per_hour." end if isnothing(critical_loads_kw) @@ -500,7 +499,7 @@ function BuiltInElectricLoad( ), ) if !(buildingtype in default_buildings) - error("buildingtype $(buildingtype) not in $(default_buildings).") + throw(@error("buildingtype $(buildingtype) not in $(default_buildings).")) end if isempty(city) diff --git a/src/core/electric_tariff.jl b/src/core/electric_tariff.jl index 8ef0a1a3e..bd9b751d8 100644 --- a/src/core/electric_tariff.jl +++ b/src/core/electric_tariff.jl @@ -85,9 +85,9 @@ end tou_energy_rates_per_kwh::Array=[], add_tou_energy_rates_to_urdb_rate::Bool=false, remove_tiers::Bool=false, - demand_lookback_months::AbstractArray{Int64, 1}=Int64[], - demand_lookback_percent::Real=0.0, - demand_lookback_range::Int=0, + demand_lookback_months::AbstractArray{Int64, 1}=Int64[], # Array of 12 binary values, indicating months in which `demand_lookback_percent` applies. If any of these is true, `demand_lookback_range` should be zero. + demand_lookback_percent::Real=0.0, # Lookback percentage. Applies to either `demand_lookback_months` with value=1, or months in `demand_lookback_range`. + demand_lookback_range::Int=0, # Number of months for which `demand_lookback_percent` applies. If not 0, `demand_lookback_months` should not be supplied. coincident_peak_load_active_time_steps::Vector{Vector{Int64}}=[Int64[]], coincident_peak_load_charge_per_kw::AbstractVector{<:Real}=Real[] ) where { @@ -101,7 +101,10 @@ end !!! note "NEM input" The `NEM` boolean is determined by the `ElectricUtility.net_metering_limit_kw`. There is no need to pass in a `NEM` value. - + +!!! note "Demand Lookback Inputs" + Cannot use both `demand_lookback_months` and `demand_lookback_range` inputs, only one or the other. + When using lookbacks, the peak demand in each month will be the greater of the peak kW in that month and the peak kW in the lookback months times the demand_lookback_percent. """ function ElectricTariff(; urdb_label::String="", @@ -121,7 +124,7 @@ function ElectricTariff(; tou_energy_rates_per_kwh::Array=[], add_tou_energy_rates_to_urdb_rate::Bool=false, remove_tiers::Bool=false, - demand_lookback_months::AbstractArray{Int64, 1}=Int64[], + demand_lookback_months::AbstractArray{Int64, 1}=Int64[], # Array of 12 binary values, indicating months in which `demand_lookback_percent` applies. If any of these is true, demand_lookback_range should be zero. demand_lookback_percent::Real=0.0, demand_lookback_range::Int=0, coincident_peak_load_active_time_steps::Vector{Vector{Int64}}=[Int64[]], @@ -181,7 +184,7 @@ function ElectricTariff(; push!(invalid_args, "length(monthly_demand_rates) must equal 12, got length $(length(monthly_demand_rates))") end if length(invalid_args) > 0 - error("Invalid argument values: $(invalid_args)") + throw(@error("Invalid ElectricTariff argument values: $(invalid_args)")) end if isempty(monthly_demand_rates) @@ -224,7 +227,16 @@ function ElectricTariff(; end else - error("Creating ElectricTariff requires at least urdb_label, urdb_response, monthly rates, annual rates, or tou_energy_rates_per_kwh.") + throw(@error("Creating ElectricTariff requires at least urdb_label, urdb_response, monthly rates, annual rates, or tou_energy_rates_per_kwh.")) + end + + # Error checks and processing for user-defined demand_lookback_months + if length(demand_lookback_months) != 0 && length(demand_lookback_months) != 12 # User provides value with incorrect length + throw(@error("Length of demand_lookback_months array must be 12.")) + elseif demand_lookback_range != 0 && length(demand_lookback_months) != 0 # If user has provided demand_lookback_months of length 12, check that range is not used + throw(@error("Cannot supply demand_lookback_months if demand_lookback_range != 0.")) + elseif demand_lookback_range == 0 && length(demand_lookback_months) == 12 + demand_lookback_months = collect(1:12)[demand_lookback_months .== 1] end if !isnothing(u) # use URDBrate @@ -261,20 +273,20 @@ function ElectricTariff(; if add_monthly_rates_to_urdb_rate if length(monthly_energy_rates) == 12 - for tier in 1:size(energy_rates, 2), mth in 1:12, ts in time_steps_monthly[mth] + for tier in axes(energy_rates, 2), mth in 1:12, ts in time_steps_monthly[mth] energy_rates[ts, tier] += monthly_energy_rates[mth] end end if length(users_monthly_demand_rates) == 12 - for tier in 1:size(monthly_demand_rates, 2), mth in 1:12 + for tier in axes(monthly_demand_rates, 2), mth in 1:12 monthly_demand_rates[mth, tier] += users_monthly_demand_rates[mth] end end end if add_tou_energy_rates_to_urdb_rate && length(tou_energy_rates_per_kwh) == size(energy_rates, 1) - for tier in 1:size(energy_rates, 2) - energy_rates[1:end, tier] += tou_energy_rates_per_kwh + for tier in axes(energy_rates, 2) + energy_rates[:, tier] += tou_energy_rates_per_kwh end end else @@ -365,16 +377,12 @@ function get_tier_with_lowest_energy_rate(u::URDBrate) ExportRate should be lowest energy cost for tiered rates. Otherwise, ExportRate can be > FuelRate, which leads REopt to export all PV energy produced. """ - tier_with_lowest_energy_cost = 1 + #TODO: can eliminate if else if confirm that u.energy_rates is always 2D if length(u.energy_tier_limits) > 1 - annual_energy_charge_sums = Float64[] - for etier in u.energy_rates - push!(annual_energy_charge_sums, sum(etier)) - end - tier_with_lowest_energy_cost = - findall(annual_energy_charge_sums .== minimum(annual_energy_charge_sums))[1] + return argmin(sum(u.energy_rates, dims=1)) + else + return 1 end - return tier_with_lowest_energy_cost end @@ -404,7 +412,7 @@ Check length of e and upsample if length(e) != N function create_export_rate(e::AbstractArray{<:Real, 1}, N::Int, ts_per_hour::Int=1) Ne = length(e) if Ne != Int(N/ts_per_hour) || Ne != N - @error "Export rates do not have correct number of entries. Must be $(N) or $(Int(N/ts_per_hour))." + throw(@error("Export rates do not have correct number of entries. Must be $(N) or $(Int(N/ts_per_hour)).")) end if Ne != N # upsample export_rates = [-1*x for x in e for ts in 1:ts_per_hour] @@ -442,24 +450,24 @@ function remove_tiers_from_urdb_rate(u::URDBrate) if length(u.energy_tier_limits) > 1 @warn "Energy rate contains tiers. Using the first tier!" end - elec_rates = vec(u.energy_rates[:,1]) + elec_rates = u.energy_rates[:,1] if u.n_monthly_demand_tiers > 1 @warn "Monthly demand rate contains tiers. Using the last tier!" end if u.n_monthly_demand_tiers > 0 - demand_rates_monthly = vec(u.monthly_demand_rates[:,u.n_monthly_demand_tiers]) + demand_rates_monthly = u.monthly_demand_rates[:,u.n_monthly_demand_tiers] else - demand_rates_monthly = vec(u.monthly_demand_rates) # 0×0 Array{Float64,2} + demand_rates_monthly = u.monthly_demand_rates # 0×0 Array{Float64,2} end if u.n_tou_demand_tiers > 1 @warn "TOU demand rate contains tiers. Using the last tier!" end if u.n_tou_demand_tiers > 0 - demand_rates = vec(u.tou_demand_rates[:,u.n_tou_demand_tiers]) + demand_rates = u.tou_demand_rates[:,u.n_tou_demand_tiers] else - demand_rates = vec(u.tou_demand_rates) + demand_rates = u.tou_demand_rates end return elec_rates, demand_rates_monthly, demand_rates diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index a68e053ee..b28f3e9c0 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -179,11 +179,11 @@ struct ElectricUtility (!isnothing(CO2_emissions_reduction_min_fraction) || !isnothing(CO2_emissions_reduction_max_fraction) || include_climate_in_objective) - error("To include CO2 costs in the objective function or enforce emissions reduction constraints, - you must either enter custom CO2 grid emissions factors or a site location within the continental U.S.") + throw(@error("To include CO2 costs in the objective function or enforce emissions reduction constraints, + you must either enter custom CO2 grid emissions factors or a site location within the continental U.S.")) elseif ekey in ["NOx", "SO2", "PM25"] && !off_grid_flag && include_health_in_objective - error("To include health costs in the objective function, you must either enter custom health - grid emissions factors or a site location within the continental U.S.") + throw(@error("To include health costs in the objective function, you must either enter custom health + grid emissions factors or a site location within the continental U.S.")) end emissions_series_dict[ekey] = zeros(8760*time_steps_per_hour) end @@ -192,19 +192,19 @@ struct ElectricUtility end if (!isempty(outage_start_time_steps) && isempty(outage_durations)) || (isempty(outage_start_time_steps) && !isempty(outage_durations)) - error("ElectricUtility inputs outage_start_time_steps and outage_durations must both be provided to model multiple outages") + throw(@error("ElectricUtility inputs outage_start_time_steps and outage_durations must both be provided to model multiple outages")) end if (outage_start_time_step == 0 && outage_end_time_step != 0) || (outage_start_time_step != 0 && outage_end_time_step == 0) - error("ElectricUtility inputs outage_start_time_step and outage_end_time_step must both be provided to model an outage") + throw(@error("ElectricUtility inputs outage_start_time_step and outage_end_time_step must both be provided to model an outage")) end if !isempty(outage_start_time_steps) if outage_start_time_step != 0 && outage_end_time_step !=0 # Warn if outage_start/end_time_step is provided and outage_start_time_steps not empty - error("Cannot supply both outage_start(/end)_time_step for deterministic outage modeling and - multiple outage_start_time_steps for stochastic outage modeling. Please use one or the other.") + throw(@error("Cannot supply both outage_start(/end)_time_step for deterministic outage modeling and + multiple outage_start_time_steps for stochastic outage modeling. Please use one or the other.")) else - @warn ("When using stochastic outage modeling (i.e. outage_start_time_steps, outage_durations, outage_probabilities), - emissions and renewable energy percentage calculations and constraints do not consider outages.") + @warn "When using stochastic outage modeling (i.e. outage_start_time_steps, outage_durations, outage_probabilities), + emissions and renewable energy percentage calculations and constraints do not consider outages." end end @@ -269,7 +269,7 @@ function region_abbreviation(latitude, longitude) end end if isnothing(abbr) - @info "Could not find AVERT region containing site latitude/longitude. Checking site proximity to AVERT regions." + @warn "Could not find AVERT region containing site latitude/longitude. Checking site proximity to AVERT regions." else return abbr, meters_to_region end diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 3ba49c9f2..7ceb2c5da 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -315,7 +315,7 @@ struct ElectricStorage <: AbstractElectricStorage if s.model_degradation if haskey(d, :replace_cost_per_kw) && d[:replace_cost_per_kw] != 0.0 || haskey(d, :replace_cost_per_kwh) && d[:replace_cost_per_kwh] != 0.0 - @warn "Setting ElectricStorage replacement costs to zero. \nUsing degradation.maintenance_cost_per_kwh instead." + @warn "Setting ElectricStorage replacement costs to zero. Using degradation.maintenance_cost_per_kwh instead." end replace_cost_per_kw = 0.0 replace_cost_per_kwh = 0.0 diff --git a/src/core/energy_storage/storage.jl b/src/core/energy_storage/storage.jl index a033fe63b..349a86afd 100644 --- a/src/core/energy_storage/storage.jl +++ b/src/core/energy_storage/storage.jl @@ -85,7 +85,7 @@ mutable struct StorageTypes elseif occursin("Cold", k) push!(cold_storage, k) else - @warn "Thermal Storage not labeled as Hot or Cold." + throw(@error("Thermal Storage not labeled as Hot or Cold.")) end end end diff --git a/src/core/existing_boiler.jl b/src/core/existing_boiler.jl index 1361b0bdb..dcaf39788 100644 --- a/src/core/existing_boiler.jl +++ b/src/core/existing_boiler.jl @@ -54,7 +54,7 @@ end production_type::String = "hot_water", max_thermal_factor_on_peak_load::Real = 1.25, efficiency::Real = NaN, - fuel_cost_per_mmbtu::Union{<:Real, AbstractVector{<:Real}} = [], + fuel_cost_per_mmbtu::Union{<:Real, AbstractVector{<:Real}} = [], # REQUIRED. Can be a scalar, a list of 12 monthly values, or a time series of values for every time step fuel_type::String = "natural_gas", # "restrict_to": ["natural_gas", "landfill_bio_gas", "propane", "diesel_oil"] can_supply_steam_turbine::Bool = false, fuel_renewable_energy_fraction::Real = get(FUEL_DEFAULTS["fuel_renewable_energy_fraction"],fuel_type,0), @@ -92,7 +92,7 @@ function ExistingBoiler(; production_type::String = "hot_water", max_thermal_factor_on_peak_load::Real = 1.25, efficiency::Real = NaN, - fuel_cost_per_mmbtu::Union{<:Real, AbstractVector{<:Real}} = [], + fuel_cost_per_mmbtu::Union{<:Real, AbstractVector{<:Real}} = [], # REQUIRED. Can be a scalar, a list of 12 monthly values, or a time series of values for every time step fuel_type::String = "natural_gas", # "restrict_to": ["natural_gas", "landfill_bio_gas", "propane", "diesel_oil"] can_supply_steam_turbine::Bool = false, fuel_renewable_energy_fraction::Real = get(FUEL_DEFAULTS["fuel_renewable_energy_fraction"],fuel_type,0), @@ -106,7 +106,7 @@ function ExistingBoiler(; @assert production_type in ["steam", "hot_water"] if isempty(fuel_cost_per_mmbtu) - throw(@error "The ExistingBoiler.fuel_cost_per_mmbtu is a required input when modeling a heating load which is served by the Existing Boiler in the BAU case") + throw(@error("The ExistingBoiler.fuel_cost_per_mmbtu is a required input when modeling a heating load which is served by the Existing Boiler in the BAU case")) end if isnan(efficiency) diff --git a/src/core/financial.jl b/src/core/financial.jl index 4d86bebc6..26e87f4d4 100644 --- a/src/core/financial.jl +++ b/src/core/financial.jl @@ -43,11 +43,11 @@ owner_discount_rate_fraction::Real = 0.0564, analysis_years::Int = 25, value_of_lost_load_per_kwh::Union{Array{R,1}, R} where R<:Real = 1.00, - microgrid_upgrade_cost_fraction::Real = off_grid_flag ? 0.0 : 0.3, # not applicable when off_grid_flag is true + microgrid_upgrade_cost_fraction::Real = off_grid_flag ? 0.0 : 0.3, # not applicable when `off_grid_flag` is true 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_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. # Emissions cost inputs CO2_cost_per_tonne::Real = 51.0, CO2_cost_escalation_rate_fraction::Real = 0.042173, @@ -121,11 +121,11 @@ struct Financial owner_discount_rate_fraction::Real = 0.0564, analysis_years::Int = 25, value_of_lost_load_per_kwh::Union{Array{<:Real,1}, Real} = 1.00, - microgrid_upgrade_cost_fraction::Real = off_grid_flag ? 0.0 : 0.3, # not applicable when off_grid_flag is true + microgrid_upgrade_cost_fraction::Real = off_grid_flag ? 0.0 : 0.3, # not applicable when `off_grid_flag` is true macrs_five_year::Array{<:Real,1} = [0.2, 0.32, 0.192, 0.1152, 0.1152, 0.0576], # IRS pub 946 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. + 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. # Emissions cost inputs CO2_cost_per_tonne::Real = 51.0, CO2_cost_escalation_rate_fraction::Real = 0.042173, @@ -145,12 +145,12 @@ struct Financial ) if off_grid_flag && !(microgrid_upgrade_cost_fraction == 0.0) - @warn "microgrid_upgrade_cost_fraction is not applied when off_grid_flag is true. Setting microgrid_upgrade_cost_fraction to 0.0." + @warn "microgrid_upgrade_cost_fraction is not applied when `off_grid_flag` is true. Setting microgrid_upgrade_cost_fraction to 0.0." microgrid_upgrade_cost_fraction = 0.0 end if !off_grid_flag && (offgrid_other_capital_costs != 0.0 || offgrid_other_annual_costs != 0.0) - @warn "offgrid_other_capital_costs and offgrid_other_annual_costs are only applied when off_grid_flag is true. Setting these inputs to 0.0 for this grid-connected analysis." + @warn "offgrid_other_capital_costs and offgrid_other_annual_costs are only applied when `off_grid_flag` is true. Setting these inputs to 0.0 for this grid-connected analysis." offgrid_other_capital_costs = 0.0 offgrid_other_annual_costs = 0.0 end @@ -200,7 +200,7 @@ struct Financial end if missing_health_inputs && include_health_in_objective - error("To include health costs in the objective function, you must either enter custom emissions costs and escalation rates or a site location within the CAMx grid.") + throw(@error("To include health costs in the objective function, you must either enter custom emissions costs and escalation rates or a site location within the CAMx grid.")) end @@ -250,7 +250,13 @@ function easiur_costs(latitude::Real, longitude::Real, grid_or_onsite::String) @warn "Error in easiur_costs: grid_or_onsite must equal either 'grid' or 'onsite'" return nothing end - EASIUR_data = get_EASIUR2005(type, pop_year=2020, income_year=2020, dollar_year=2010) + EASIUR_data = nothing + try + EASIUR_data = get_EASIUR2005(type, pop_year=2020, income_year=2020, dollar_year=2010) + catch e + @warn "Could not look up EASIUR health costs from point ($latitude,$longitude). {$e}" + return nothing + end # convert lon, lat to CAMx grid (x, y), specify datum. default is NAD83 # Note: x, y returned from g2l follows the CAMx grid convention. @@ -268,15 +274,21 @@ function easiur_costs(latitude::Real, longitude::Real, grid_or_onsite::String) ) return costs_per_tonne catch - @error "Could not look up EASIUR health costs from point ($latitude,$longitude). Location is likely invalid or outside the CAMx grid." + @warn "Could not look up EASIUR health costs from point ($latitude,$longitude). Location is likely invalid or outside the CAMx grid." return nothing end end function easiur_escalation_rates(latitude::Real, longitude::Real, inflation::Real) - EASIUR_150m_yr2020 = get_EASIUR2005("p150", pop_year=2020, income_year=2020, dollar_year=2010) - EASIUR_150m_yr2024 = get_EASIUR2005("p150", pop_year=2024, income_year=2024, dollar_year=2010) - + EASIUR_150m_yr2020 = nothing + EASIUR_150m_yr2024 = nothing + try + EASIUR_150m_yr2020 = get_EASIUR2005("p150", pop_year=2020, income_year=2020, dollar_year=2010) + EASIUR_150m_yr2024 = get_EASIUR2005("p150", pop_year=2024, income_year=2024, dollar_year=2010) + catch e + @warn "Could not look up EASIUR health cost escalation rates from point ($latitude,$longitude). {$e}" + return nothing + end # convert lon, lat to CAMx grid (x, y), specify datum. default is NAD83 coords = g2l(longitude, latitude, datum="NAD83") x = Int(round(coords[1])) @@ -291,7 +303,7 @@ function easiur_escalation_rates(latitude::Real, longitude::Real, inflation::Rea ) return escalation_rates catch - @error "Could not look up EASIUR health cost escalation rates from point ($latitude,$longitude). Location is likely invalid or outside the CAMx grid" + @warn "Could not look up EASIUR health cost escalation rates from point ($latitude,$longitude). Location is likely invalid or outside the CAMx grid" return nothing end end @@ -387,7 +399,7 @@ function get_EASIUR2005(stack::String; pop_year::Int64=2005, income_year::Int64= ) if !(stack in ["area", "p150", "p300"]) - @error "stack should be one of 'area', 'p150', 'p300'" + throw(@error("stack should be one of 'area', 'p150', 'p300'")) return nothing end @@ -407,7 +419,7 @@ function get_EASIUR2005(stack::String; pop_year::Int64=2005, income_year::Int64= setindex!(ret_map, v .* adj, k) end catch - @error "income year is $(income_year) but must be between 1990 to 2024" + throw(@error("income year is $(income_year) but must be between 1990 to 2024")) return nothing end end @@ -418,7 +430,7 @@ function get_EASIUR2005(stack::String; pop_year::Int64=2005, income_year::Int64= setindex!(ret_map, v .* adj, k) end catch e - @error "Dollar year must be between 1980 to 2010" + throw(@error("Dollar year must be between 1980 to 2010")) return nothing end end diff --git a/src/core/flexible_hvac.jl b/src/core/flexible_hvac.jl index fa454d7d9..4f83011c7 100644 --- a/src/core/flexible_hvac.jl +++ b/src/core/flexible_hvac.jl @@ -74,9 +74,7 @@ constructor then the conversion to Matrices will be done appropriately. The `ExistingChiller` is electric and so its operating cost is determined by the `ElectricTariff`. !!! note - The `ExistingBoiler` default operating cost is zero. Please provide the `fuel_cost_per_mmbtu` field - for the `ExistingBoiler` if you want non-zero BAU heating costs. The `fuel_cost_per_mmbtu` can be - a scalar, a list of 12 monthly values, or a time series of values for every time step. + BAU heating costs will be determined by the `ExistingBoiler` inputs, including`fuel_cost_per_mmbtu`. """ struct FlexibleHVAC system_matrix::AbstractMatrix{Float64} # N x N, with N states (temperatures in RC network) diff --git a/src/core/ghp.jl b/src/core/ghp.jl index 6041c358a..7284dd4fd 100644 --- a/src/core/ghp.jl +++ b/src/core/ghp.jl @@ -205,7 +205,7 @@ function assign_thermal_factor!(d::Dict, heating_or_cooling::String) factor_data = CSV.read("../data/ghp/ghp_cooling_efficiency_thermal_factors.csv", DataFrame) building_type = get(d["CoolingLoad"], "doe_reference_name", []) else - @error("Specify `space_heating` or `cooling` for assign_thermal_factor! function") + throw(@error("Specify `space_heating` or `cooling` for assign_thermal_factor! function")) end latitude = d["Site"]["latitude"] longitude = d["Site"]["longitude"] diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index e62182853..4af6c7657 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -69,25 +69,25 @@ struct DomesticHotWaterLoad if length(fuel_loads_mmbtu_per_hour) > 0 if !(length(fuel_loads_mmbtu_per_hour) / time_steps_per_hour ≈ 8760) - throw(@error "Provided domestic hot water load does not match the time_steps_per_hour.") + throw(@error("Provided DomesticHotWaterLoad `fuel_loads_mmbtu_per_hour` does not match the time_steps_per_hour.")) end loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * EXISTING_BOILER_EFFICIENCY) .* addressable_load_fraction if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0 - @warn "DomesticHotWaterLoad fuel_loads_mmbtu_per_hour was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored." + @warn "DomesticHotWaterLoad `fuel_loads_mmbtu_per_hour` was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored." end if length(addressable_load_fraction) > 1 if length(addressable_load_fraction) != length(fuel_loads_mmbtu_per_hour) - error("`addressable_load_fraction`` must be a scalar or an array of length `fuel_loads_mmbtu_per_hour`") + throw(@error("`addressable_load_fraction` must be a scalar or an array of length `fuel_loads_mmbtu_per_hour`")) end end elseif !isempty(doe_reference_name) if length(addressable_load_fraction) > 1 if !isempty(monthly_mmbtu) && length(addressable_load_fraction) != 12 - error("`addressable_load_fraction`` must be a scalar or an array of length 12 if `monthly_mmbtu` is input") + throw(@error("`addressable_load_fraction` must be a scalar or an array of length 12 if `monthly_mmbtu` is input")) end end @@ -101,14 +101,13 @@ struct DomesticHotWaterLoad blended_doe_reference_names, blended_doe_reference_percents, city, annual_mmbtu, monthly_mmbtu, addressable_load_fraction) else - error("Cannot construct DomesticHotWaterLoad. You must provide either [fuel_loads_mmbtu_per_hour], - [doe_reference_name, city], - or [blended_doe_reference_names, blended_doe_reference_percents, city].") + throw(@error("Cannot construct DomesticHotWaterLoad. You must provide either [fuel_loads_mmbtu_per_hour], + [doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city].")) end if length(loads_kw) < 8760*time_steps_per_hour loads_kw = repeat(loads_kw, inner=Int(time_steps_per_hour / (length(loads_kw)/8760))) - @info "Repeating domestic hot water loads in each hour to match the time_steps_per_hour." + @warn "Repeating domestic hot water loads in each hour to match the time_steps_per_hour." end new( @@ -172,7 +171,7 @@ struct SpaceHeatingLoad if length(fuel_loads_mmbtu_per_hour) > 0 if !(length(fuel_loads_mmbtu_per_hour) / time_steps_per_hour ≈ 8760) - throw(@error "Provided space heating load does not match the time_steps_per_hour.") + throw(@error("Provided space heating load does not match the time_steps_per_hour.")) end loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * EXISTING_BOILER_EFFICIENCY) .* addressable_load_fraction @@ -192,14 +191,13 @@ struct SpaceHeatingLoad blended_doe_reference_names, blended_doe_reference_percents, city, annual_mmbtu, monthly_mmbtu, addressable_load_fraction) else - error("Cannot construct BuiltInSpaceHeatingLoad. You must provide either [fuel_loads_mmbtu_per_hour], - [doe_reference_name, city], - or [blended_doe_reference_names, blended_doe_reference_percents, city].") + throw(@error("Cannot construct BuiltInSpaceHeatingLoad. You must provide either [fuel_loads_mmbtu_per_hour], + [doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city].")) end if length(loads_kw) < 8760*time_steps_per_hour loads_kw = repeat(loads_kw, inner=Int(time_steps_per_hour / (length(loads_kw)/8760))) - @info "Repeating space heating loads in each hour to match the time_steps_per_hour." + @warn "Repeating space heating loads in each hour to match the time_steps_per_hour." end new( @@ -304,19 +302,19 @@ struct CoolingLoad loads_kw = nothing if length(thermal_loads_ton) > 0 if !(length(thermal_loads_ton) / time_steps_per_hour ≈ 8760) - error("Provided cooling load does not match the time_steps_per_hour.") + throw(@error("Provided cooling load does not match the time_steps_per_hour.")) end loads_kw_thermal = thermal_loads_ton .* (KWH_THERMAL_PER_TONHOUR) elseif !isempty(per_time_step_fractions_of_electric_load) && (length(site_electric_load_profile) / time_steps_per_hour ≈ 8760) if !(length(per_time_step_fractions_of_electric_load) / time_steps_per_hour ≈ 8760) - @error "Provided cooling per_time_step_fractions_of_electric_load array does not match the time_steps_per_hour." + throw(@error("Provided cooling per_time_step_fractions_of_electric_load array does not match the time_steps_per_hour.")) end loads_kw = per_time_step_fractions_of_electric_load .* site_electric_load_profile elseif !isempty(monthly_fractions_of_electric_load) && (length(site_electric_load_profile) / time_steps_per_hour ≈ 8760) if !(length(monthly_fractions_of_electric_load) ≈ 12) - @error "Provided cooling monthly_fractions_of_electric_load array does not have 12 values." + throw(@error("Provided cooling monthly_fractions_of_electric_load array does not have 12 values.")) end timeseries = collect(DateTime(2017,1,1) : Minute(60/time_steps_per_hour) : DateTime(2017,1,1) + Minute(8760*60 - 60/time_steps_per_hour)) @@ -344,7 +342,7 @@ struct CoolingLoad modified_fraction = default_fraction * blended_doe_reference_percents[i] if length(site_electric_load_profile) > 8784 modified_fraction = repeat(modified_fraction, inner=time_steps_per_hour / (length(site_electric_load_profile)/8760)) - @info "Repeating cooling electric load in each hour to match the time_steps_per_hour." + @warn "Repeating cooling electric load in each hour to match the time_steps_per_hour." end loads_kw += site_electric_load_profile .* modified_fraction end @@ -356,9 +354,9 @@ struct CoolingLoad end else - error("Cannot construct BuiltInCoolingLoad. You must provide either [thermal_loads_ton], + throw(@error("Cannot construct BuiltInCoolingLoad. You must provide either [thermal_loads_ton], [doe_reference_name, city], [blended_doe_reference_names, blended_doe_reference_percents, city], - or the site_electric_load_profile along with one of [per_time_step_fractions_of_electric_load, monthly_fractions_of_electric_load, annual_fraction_of_electric_load].") + or the site_electric_load_profile along with one of [per_time_step_fractions_of_electric_load, monthly_fractions_of_electric_load, annual_fraction_of_electric_load].")) end if isnothing(loads_kw_thermal) # have to convert electric loads_kw to thermal load @@ -385,7 +383,7 @@ struct CoolingLoad if length(loads_kw_thermal) < 8760*time_steps_per_hour loads_kw_thermal = repeat(loads_kw_thermal, inner=Int(time_steps_per_hour / (length(loads_kw_thermal)/8760))) - @info "Repeating cooling loads in each hour to match the time_steps_per_hour." + @warn "Repeating cooling loads in each hour to match the time_steps_per_hour." end new( @@ -726,7 +724,7 @@ function BuiltInDomesticHotWaterLoad( city = find_ashrae_zone_city(latitude, longitude) end if !(buildingtype in default_buildings) - error("buildingtype $(buildingtype) not in $(default_buildings).") + throw(@error("buildingtype $(buildingtype) not in $(default_buildings).")) end if isnothing(annual_mmbtu) # Use FlatLoad annual_mmbtu from data for all types of FlatLoads because we don't have separate data for e.g. FlatLoad_16_7 @@ -1065,7 +1063,7 @@ function BuiltInSpaceHeatingLoad( city = find_ashrae_zone_city(latitude, longitude) end if !(buildingtype in default_buildings) - error("buildingtype $(buildingtype) not in $(default_buildings).") + throw(@error("buildingtype $(buildingtype) not in $(default_buildings).")) end if isnothing(annual_mmbtu) # Use FlatLoad annual_mmbtu from data for all types of FlatLoads because we don't have separate data for e.g. FlatLoad_16_7 @@ -1404,7 +1402,7 @@ function BuiltInCoolingLoad( city = find_ashrae_zone_city(latitude, longitude) end if !(buildingtype in default_buildings) - error("buildingtype $(buildingtype) not in $(default_buildings).") + throw(@error("buildingtype $(buildingtype) not in $(default_buildings).")) end # Set initial existing_chiller_cop to "cop_unknown_thermal" if not passed in; we will update existing_chiller_cop once the load profile is determined if isnothing(existing_chiller_cop) diff --git a/src/core/production_factor.jl b/src/core/production_factor.jl index 434f69865..429563867 100644 --- a/src/core/production_factor.jl +++ b/src/core/production_factor.jl @@ -58,19 +58,19 @@ function get_production_factor(pv::PV, latitude::Real, longitude::Real; timefram r = HTTP.get(url) response = JSON.parse(String(r.body)) if r.status != 200 - error("Bad response from PVWatts: $(response["errors"])") + throw(@error("Bad response from PVWatts: $(response["errors"])")) end @info "PVWatts success." watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W) if length(watts) != 8760 - @error "PVWatts did not return a valid production factor. Got $watts" + throw(@error("PVWatts did not return a valid production factor. Got $watts")) end if time_steps_per_hour > 1 watts = repeat(watts, inner=time_steps_per_hour) end return watts catch e - @error "Error occurred when calling PVWatts: $e" + throw(@error("Error occurred when calling PVWatts: $e")) end end @@ -132,17 +132,17 @@ function get_production_factor(wind::Wind, latitude::Real, longitude::Real, time @info "Querying Wind Toolkit for resource data ..." r = HTTP.get(url; retries=5) if r.status != 200 - error("Bad response from Wind Toolkit: $(response["errors"])") + throw(@error("Bad response from Wind Toolkit: $(response["errors"])")) end @info "Wind Toolkit success." resource = readdlm(IOBuffer(String(r.body)), ',', Float64, '\n'; skipstart=5); # columns: Temperature, Pressure, Speed, Direction (C, atm, m/s, Degrees) if size(resource) != (8760, 4) - @error "Wind Toolkit did not return valid resource data. Got an array with size $(size(resource))" + throw(@error("Wind Toolkit did not return valid resource data. Got an array with size $(size(resource))")) end catch e - @error "Error occurred when calling Wind Toolkit: $e" + throw(@error("Error occurred when calling Wind Toolkit: $e")) end push!(resources, resource) end @@ -192,8 +192,8 @@ function get_production_factor(wind::Wind, latitude::Real, longitude::Real, time elseif Sys.iswindows() libfile = "ssc.dll" else - @error """Unsupported platform for using the SAM Wind module. - You can alternatively provide the Wind.production_factor_series""" + throw(@error("Unsupported platform for using the SAM Wind module. + You can alternatively provide the Wind `prod_factor_series`")) end global hdl = joinpath(@__DIR__, "..", "sam", libfile) @@ -290,7 +290,7 @@ function get_production_factor(wind::Wind, latitude::Real, longitude::Real, time try msg = unsafe_string(msg_ptr) finally - @error("SAM Wind simulation error: $msg") + throw(@error("SAM Wind simulation error: $msg")) end end @@ -304,12 +304,11 @@ function get_production_factor(wind::Wind, latitude::Real, longitude::Real, time @ccall hdl.ssc_data_free(data::Ptr{Cvoid})::Cvoid catch e - @error "Problem calling SAM C library!" - showerror(stdout, e) + throw(@error("Problem calling SAM C library! $e")) end if !(length(sam_prodfactor) == 8760) - @error "Wind production factor from SAM has length $(length(sam_prodfactor)) (should be 8760)." + throw(@error("Wind production factor from SAM has length $(length(sam_prodfactor)) (should be 8760).")) end @assert !(nothing in sam_prodfactor) "Did not get complete Wind production factor from SAM." diff --git a/src/core/pv.jl b/src/core/pv.jl index dcf71c9eb..ca1bd5b85 100644 --- a/src/core/pv.jl +++ b/src/core/pv.jl @@ -174,12 +174,12 @@ struct PV <: AbstractTech ) if !(off_grid_flag) && !(operating_reserve_required_fraction == 0.0) - @warn "PV operating_reserve_required_fraction applies only when off_grid_flag is True. Setting operating_reserve_required_fraction to 0.0 for this on-grid analysis." + @warn "PV operating_reserve_required_fraction applies only when true. Setting operating_reserve_required_fraction to 0.0 for this on-grid analysis." operating_reserve_required_fraction = 0.0 end if off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) - @warn "Net metering, wholesale, and grid exports are not possible for off-grid scenarios. Setting PV can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False." + @warn "Setting PV can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." can_net_meter = false can_wholesale = false can_export_beyond_nem_limit = false @@ -216,7 +216,7 @@ struct PV <: AbstractTech end # TODO validate additional args if length(invalid_args) > 0 - error("Invalid argument values: $(invalid_args)") + throw(@error("Invalid PV argument values: $(invalid_args)")) end new( diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 26a57a174..97904534b 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -35,7 +35,17 @@ Return REoptInputs(s) where s in `Scenario` defined in dict `d`. """ function REoptInputs(d::Dict) - REoptInputs(Scenario(d)) + + # Keep try catch to support API v3 call to `REoptInputs` + try + REoptInputs(Scenario(d)) + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) + end + end end """ @@ -44,8 +54,17 @@ end Solve the model using the `Scenario` defined in JSON file stored at the file path `fp`. """ function run_reopt(m::JuMP.AbstractModel, fp::String) - s = Scenario(JSON.parsefile(fp)) - run_reopt(m, REoptInputs(s)) + + try + s = Scenario(JSON.parsefile(fp)) + run_reopt(m, REoptInputs(s)) + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) + end + end end @@ -55,8 +74,17 @@ end Solve the model using the `Scenario` defined in dict `d`. """ function run_reopt(m::JuMP.AbstractModel, d::Dict) - s = Scenario(d) - run_reopt(m, REoptInputs(s)) + + try + s = Scenario(d) + run_reopt(m, REoptInputs(s)) + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) + end + end end @@ -66,10 +94,19 @@ end Solve the model using a `Scenario` or `BAUScenario`. """ function run_reopt(m::JuMP.AbstractModel, s::AbstractScenario) - if s.site.CO2_emissions_reduction_min_fraction > 0.0 || s.site.CO2_emissions_reduction_max_fraction < 1.0 - error("To constrain CO2 emissions reduction min or max percentages, the optimal and business as usual scenarios must be run in parallel. Use a version of run_reopt() that takes an array of two models.") + + try + if s.site.CO2_emissions_reduction_min_fraction > 0.0 || s.site.CO2_emissions_reduction_max_fraction < 1.0 + throw(@error("To constrain CO2 emissions reduction min or max percentages, the optimal and business as usual scenarios must be run in parallel. Use a version of run_reopt() that takes an array of two models.")) + end + run_reopt(m, REoptInputs(s)) + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) + end end - run_reopt(m, REoptInputs(s)) end @@ -80,7 +117,7 @@ Method for use with Threads when running BAU in parallel with optimal scenario. """ function run_reopt(t::Tuple{JuMP.AbstractModel, AbstractInputs}) run_reopt(t[1], t[2]; organize_pvs=false) - # must organize_pvs after adding proforma results + # must organize_pvs after adding proforma results end @@ -91,7 +128,7 @@ Solve the `Scenario` and `BAUScenario` in parallel using the first two (empty) m JSON file at the filepath `fp`. """ function run_reopt(ms::AbstractArray{T, 1}, fp::String) where T <: JuMP.AbstractModel - d = JSON.parsefile(fp) + d = JSON.parsefile(fp) run_reopt(ms, d) end @@ -102,39 +139,56 @@ end Solve the `Scenario` and `BAUScenario` in parallel using the first two (empty) models in `ms` and inputs from `d`. """ function run_reopt(ms::AbstractArray{T, 1}, d::Dict) where T <: JuMP.AbstractModel - s = Scenario(d) - if s.settings.off_grid_flag - @warn "Only using first Model and not running BAU case because Settings.off_grid_flag == true. The BAU scenario is not applicable for off-grid microgrids." - results = run_reopt(ms[1], s) - return results - end - run_reopt(ms, REoptInputs(s)) + try + s = Scenario(d) + if s.settings.off_grid_flag + @warn "Only using first Model and not running BAU case because `off_grid_flag` is true. The BAU scenario is not applicable for off-grid microgrids." + results = run_reopt(ms[1], s) + return results + end + + run_reopt(ms, REoptInputs(s)) + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) + end + end end - """ run_reopt(ms::AbstractArray{T, 1}, p::REoptInputs) where T <: JuMP.AbstractModel Solve the `Scenario` and `BAUScenario` in parallel using the first two (empty) models in `ms` and inputs from `p`. """ function run_reopt(ms::AbstractArray{T, 1}, p::REoptInputs) where T <: JuMP.AbstractModel - bau_inputs = BAUInputs(p) - inputs = ((ms[1], bau_inputs), (ms[2], p)) - rs = Any[0, 0] - Threads.@threads for i = 1:2 - rs[i] = run_reopt(inputs[i]) - end - if typeof(rs[1]) <: Dict && typeof(rs[2]) <: Dict - # TODO when a model is infeasible the JuMP.Model is returned from run_reopt (and not the results Dict) - results_dict = combine_results(p, rs[1], rs[2], bau_inputs.s) - results_dict["Financial"] = merge(results_dict["Financial"], proforma_results(p, results_dict)) - if !isempty(p.techs.pv) - organize_multiple_pv_results(p, results_dict) + + try + bau_inputs = BAUInputs(p) + inputs = ((ms[1], bau_inputs), (ms[2], p)) + rs = Any[0, 0] + Threads.@threads for i = 1:2 + rs[i] = run_reopt(inputs[i]) + end + if typeof(rs[1]) <: Dict && typeof(rs[2]) <: Dict + # TODO when a model is infeasible the JuMP.Model is returned from run_reopt (and not the results Dict) + results_dict = combine_results(p, rs[1], rs[2], bau_inputs.s) + results_dict["Financial"] = merge(results_dict["Financial"], proforma_results(p, results_dict)) + if !isempty(p.techs.pv) + organize_multiple_pv_results(p, results_dict) + end + return results_dict + else + return rs + end + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) end - return results_dict - else - return rs end end @@ -198,7 +252,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) elseif b in p.s.storage.types.cold add_cold_thermal_storage_dispatch_constraints(m, p, b) else - @error("Invalid storage does not fall in a thermal or electrical set") + throw(@error("Invalid storage does not fall in a thermal or electrical set")) end end end @@ -270,7 +324,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) end if !isempty(p.techs.pbi) - @warn "adding binary variable(s) to model production based incentives" + @warn "Adding binary variable(s) to model production based incentives" add_prod_incent_vars_and_constraints(m, p) end end @@ -315,7 +369,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) end if !isempty(p.techs.segmented) - @warn "adding binary variable(s) to model cost curves" + @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 * ( @@ -403,10 +457,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) # Comfort limit violation costs m[:dvComfortLimitViolationCost] + - # Additional annual costs, tax deductible for owner (only applies when off_grid_flag is true) + # Additional annual costs, tax deductible for owner (only applies when `off_grid_flag` is true) p.s.financial.offgrid_other_annual_costs * p.pwf_om * (1 - p.s.financial.owner_tax_rate_fraction) + - # Additional capital costs, depreciable (only applies when off_grid_flag is true) + # Additional capital costs, depreciable (only applies when `off_grid_flag` is true) m[:OffgridOtherCapexAfterDepr] ); @@ -447,34 +501,47 @@ end function run_reopt(m::JuMP.AbstractModel, p::REoptInputs; organize_pvs=true) - build_reopt!(m, p) - - @info "Model built. Optimizing..." - tstart = time() - optimize!(m) - opt_time = round(time() - tstart, digits=3) - if termination_status(m) == MOI.TIME_LIMIT - status = "timed-out" - elseif termination_status(m) == MOI.OPTIMAL - status = "optimal" - else - status = "not optimal" - @warn "REopt solved with " termination_status(m), ", returning the model." - return m + try + build_reopt!(m, p) + + @info "Model built. Optimizing..." + tstart = time() + optimize!(m) + opt_time = round(time() - tstart, digits=3) + if termination_status(m) == MOI.TIME_LIMIT + status = "timed-out" + elseif termination_status(m) == MOI.OPTIMAL + status = "optimal" + else + status = "not optimal" + @warn "REopt solved with " termination_status(m), ", returning the model." + return m + end + @info "REopt solved with " termination_status(m) + @info "Solving took $(opt_time) seconds." + + tstart = time() + results = reopt_results(m, p) + time_elapsed = time() - tstart + @info "Results processing took $(round(time_elapsed, digits=3)) seconds." + results["status"] = status + results["solver_seconds"] = opt_time + + if organize_pvs && !isempty(p.techs.pv) # do not want to organize_pvs when running BAU case in parallel b/c then proform code fails + organize_multiple_pv_results(p, results) + end + + # add error messages (if any) and warnings to results dict + results["Messages"] = logger_to_dict() + + return results + catch e + if isnothing(e) # Error thrown by REopt + handle_errors() + else + handle_errors(e, stacktrace(catch_backtrace())) + end end - @info "REopt solved with " termination_status(m) - @info "Solving took $(opt_time) seconds." - - tstart = time() - results = reopt_results(m, p) - time_elapsed = time() - tstart - @info "Results processing took $(round(time_elapsed, digits=3)) seconds." - results["status"] = status - results["solver_seconds"] = opt_time - if organize_pvs && !isempty(p.techs.pv) # do not want to organize_pvs when running BAU case in parallel b/c then proform code fails - organize_multiple_pv_results(p, results) - end - return results end @@ -503,8 +570,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) end if !isempty(p.techs.gen) # Problem becomes a MILP - @warn """Adding binary variable to model gas generator. - Some solvers are very slow with integer variables""" + @warn "Adding binary variable to model gas generator. Some solvers are very slow with integer variables." @variables m begin binGenIsOnInTS[p.techs.gen, p.time_steps], Bin # 1 If technology t is operating in time step h; 0 otherwise end @@ -519,8 +585,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) end if !(p.s.electric_utility.allow_simultaneous_export_import) & !isempty(p.s.electric_tariff.export_bins) - @warn """Adding binary variable to prevent simultaneous grid import/export. - Some solvers are very slow with integer variables""" + @warn "Adding binary variable to prevent simultaneous grid import/export. Some solvers are very slow with integer variables" @variable(m, binNoGridPurchases[p.time_steps], Bin) end @@ -540,8 +605,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) end if !isempty(p.s.electric_utility.outage_durations) # add dvUnserved Load if there is at least one outage - @warn """Adding binary variable to model outages. - Some solvers are very slow with integer variables""" + @warn "Adding binary variable to model outages. Some solvers are very slow with integer variables" max_outage_duration = maximum(p.s.electric_utility.outage_durations) outage_time_steps = p.s.electric_utility.outage_time_steps tZeros = p.s.electric_utility.outage_start_time_steps diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 74e3dba57..e3fbf96d1 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -699,8 +699,8 @@ function setup_absorption_chiller_inputs(s::AbstractScenario, max_sizes, min_siz if isnothing(s.chp) thermal_factor = 1.0 elseif s.chp.cooling_thermal_factor == 0.0 - error("The CHP cooling_thermal_factor is 0.0 which implies that CHP cannot serve AbsorptionChiller. If you - want to model CHP and AbsorptionChiller, you must specify a cooling_thermal_factor greater than 0.0") + throw(@error("The CHP cooling_thermal_factor is 0.0 which implies that CHP cannot serve AbsorptionChiller. If you + want to model CHP and AbsorptionChiller, you must specify a cooling_thermal_factor greater than 0.0")) else thermal_factor = s.chp.cooling_thermal_factor end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 1a1eb8c57..9fa389cca 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -84,6 +84,9 @@ All values of `d` are expected to be `Dicts` except for `PV` and `GHP`, which ca handle conversion of Vector of Vectors from JSON to a Matrix in Julia). """ function Scenario(d::Dict; flex_hvac_from_json=false) + + instantiate_logger() + d = deepcopy(d) if haskey(d, "Settings") settings = Settings(;dictkeys_tosymbols(d["Settings"])...) @@ -98,7 +101,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) offgrid_allowed_keys = ["PV", "Wind", "ElectricStorage", "Generator", "Settings", "Site", "Financial", "ElectricLoad", "ElectricTariff", "ElectricUtility"] unallowed_keys = setdiff(keys(d), offgrid_allowed_keys) if !isempty(unallowed_keys) - error("Currently, only PV, ElectricStorage, and Generator can be modeled when off_grid_flag is true. Cannot model $unallowed_keys.") + throw(@error("The following key(s) are not permitted when `off_grid_flag` is true: $unallowed_keys.")) end end @@ -116,7 +119,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) push!(pvs, PV(;dictkeys_tosymbols(d["PV"])..., off_grid_flag = settings.off_grid_flag, latitude=site.latitude)) else - error("PV input must be Dict or Dict[].") + throw(@error("PV input must be Dict or Dict[].")) end end @@ -148,7 +151,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ) elseif settings.off_grid_flag if haskey(d, "ElectricUtility") - @warn "ElectricUtility inputs are not applicable when off_grid_flag is true and any ElectricUtility inputs will be ignored. For off-grid scenarios, a year-long outage will always be modeled." + @warn "ElectricUtility inputs are not applicable when `off_grid_flag` is true and will be ignored. For off-grid scenarios, a year-long outage will always be modeled." end electric_utility = ElectricUtility(; outage_start_time_step = 1, outage_end_time_step = settings.time_steps_per_hour * 8760, @@ -191,7 +194,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ) else # if ElectricTariff inputs supplied for off-grid, will not be applied. if haskey(d, "ElectricTariff") - @warn "ElectricTariff inputs are not applicable when off_grid_flag is true, and will be ignored." + @warn "ElectricTariff inputs are not applicable when `off_grid_flag` is true, and will be ignored." end electric_tariff = ElectricTariff(; blended_annual_energy_rate = 0.0, blended_annual_demand_rate = 0.0, @@ -378,11 +381,11 @@ function Scenario(d::Dict; flex_hvac_from_json=false) if length(cooling_elec_too_high_timesteps) > 0 cooling_elec_too_high_kw = cooling_elec[cooling_elec_too_high_timesteps] total_elec_when_cooling_elec_too_high = electric_load.loads_kw[cooling_elec_too_high_timesteps] - error("Cooling electric consumption cannot be more than the total electric load at any time step. At time steps + throw(@error("Cooling electric consumption cannot be more than the total electric load at any time step. At time steps $cooling_elec_too_high_timesteps the cooling electric consumption is $cooling_elec_too_high_kw (kW) and the total electric load is $total_elec_when_cooling_elec_too_high (kW). Note you may consider adjusting cooling load input versus the total electric load if you provided inputs in units of cooling tons, or - check the electric chiller COP input value.") + check the electric chiller COP input value.")) end else cooling_load = CoolingLoad(; @@ -425,7 +428,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) get_ghpghx_from_input = true end elseif haskey(d, "GHP") && !haskey(d["GHP"],"building_sqft") - error("If evaluating GHP you must enter a building_sqft") + throw(@error("If evaluating GHP you must enter a building_sqft.")) end # Modify Heating and Cooling loads for GHP retrofit to account for HVAC VAV efficiency gains if eval_ghp @@ -460,16 +463,16 @@ function Scenario(d::Dict; flex_hvac_from_json=false) r = HTTP.get(url) response = JSON.parse(String(r.body)) if r.status != 200 - error("Bad response from PVWatts: $(response["errors"])") + throw(@error("Bad response from PVWatts: $(response["errors"])")) end @info "PVWatts success." temp_c = get(response["outputs"], "tamb", []) if length(temp_c) != 8760 || isempty(temp_c) - @error "PVWatts did not return a valid temperature profile. Got $temp_c" + throw(@error("PVWatts did not return a valid temperature profile. Got $temp_c")) end ambient_temperature_f = temp_c * 1.8 .+ 32.0 catch e - @error "Error occurred when calling PVWatts: $e" + throw(@error("Error occurred when calling PVWatts: $e")) end end @@ -510,8 +513,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) JSON.print(f, ghpghx_response) end catch - error("The GhpGhx package was not added (add https://github.com/NREL/GhpGhx.jl) or - loaded (using GhpGhx) to the active Julia environment") + throw(@error("The GhpGhx package was not added (add https://github.com/NREL/GhpGhx.jl) or + loaded (using GhpGhx) to the active Julia environment")) end end # If ghpghx_responses is included in inputs, do NOT run GhpGhx.jl model and use already-run ghpghx result as input to REopt diff --git a/src/core/simulated_load.jl b/src/core/simulated_load.jl index f33cf5b62..e44646bd2 100644 --- a/src/core/simulated_load.jl +++ b/src/core/simulated_load.jl @@ -17,7 +17,7 @@ function simulated_load(d::Dict) latitude = get(d, "latitude", nothing) longitude = get(d, "longitude", nothing) if isnothing(latitude) || isnothing(longitude) - throw(@error "latitude and longitude must be provided") + throw(@error("latitude and longitude must be provided")) end load_type = get(d, "load_type", nothing) @@ -34,10 +34,10 @@ function simulated_load(d::Dict) percent_share_list = percent_share_input end if length(percent_share_list) != length(doe_reference_name) - throw(@error "The number of percent_share entries does not match that of the number of doe_reference_name entries") + throw(@error("The number of percent_share entries does not match that of the number of doe_reference_name entries")) end elseif !isnothing(doe_reference_name_input) && typeof(doe_reference_name_input) <: Vector{} && isempty(percent_share_input) # Vector of doe_reference_name but no percent_share, as needed - throw(@error "Must provide percent_share list if modeling a blended/hybrid set of buildings") + throw(@error("Must provide percent_share list if modeling a blended/hybrid set of buildings")) else doe_reference_name = doe_reference_name_input end @@ -56,10 +56,10 @@ function simulated_load(d::Dict) cooling_pct_share_list = cooling_pct_share_input end if length(cooling_pct_share_list) != length(cooling_doe_ref_name) - throw(@error "The number of cooling_pct_share entries does not match that of the number of cooling_doe_ref_name entries") + throw(@error("The number of cooling_pct_share entries does not match that of the number of cooling_doe_ref_name entries")) end elseif typeof(cooling_doe_ref_name_input) <: Vector{} && isempty(cooling_pct_share_input) # Vector of cooling_doe_ref_name but no cooling_pct_share_input, as needed - throw(@error "Must provide cooling_pct_share list if modeling a blended/hybrid set of buildings") + throw(@error("Must provide cooling_pct_share list if modeling a blended/hybrid set of buildings")) else cooling_doe_ref_name = nothing cooling_pct_share_list = Real[] @@ -73,7 +73,7 @@ function simulated_load(d::Dict) if !isnothing(doe_reference_name) for drn in doe_reference_name if !(drn in default_buildings) - throw(@error "Invalid doe_reference_name - $doe_reference_name. Select from the following: $default_buildings") + throw(@error("Invalid doe_reference_name - $doe_reference_name. Select from the following: $default_buildings")) end end end @@ -83,15 +83,15 @@ function simulated_load(d::Dict) end if latitude > 90 || latitude < -90 - throw(@error "latitude $latitude is out of acceptable range (-90 <= latitude <= 90)") + throw(@error("latitude $latitude is out of acceptable range (-90 <= latitude <= 90)")) end if longitude > 180 || longitude < -180 - throw(@error "longitude $longitude is out of acceptable range (-180 <= longitude <= 180)") + throw(@error("longitude $longitude is out of acceptable range (-180 <= longitude <= 180)")) end if !(load_type in ["electric","heating","cooling"]) - throw(@error "load_type parameter must be one of the following: 'electric', 'heating', or 'cooling'. If load_type is not specified, 'electric' is assumed.") + throw(@error("load_type parameter must be one of the following: 'electric', 'heating', or 'cooling'. If load_type is not specified, 'electric' is assumed.")) end # The following is possibly used in both load_type == "electric" and "cooling", so have to bring it out of those if-statements @@ -106,11 +106,11 @@ function simulated_load(d::Dict) if load_type == "electric" for key in keys(d) if occursin("_mmbtu", key) || occursin("_ton", key) || occursin("_fraction", key) - throw(@error "Invalid key $key for load_type=electric") + throw(@error("Invalid key $key for load_type=electric")) end end if isnothing(doe_reference_name) - throw(@error "Please supply a doe_reference_name and optionally scaling parameters (annual_kwh or monthly_totals_kwh).") + throw(@error("Please supply a doe_reference_name and optionally scaling parameters (annual_kwh or monthly_totals_kwh).")) end # Annual loads (default is nothing) annual_kwh = get(d, "annual_kwh", nothing) @@ -118,7 +118,7 @@ function simulated_load(d::Dict) monthly_totals_kwh = get(d, "monthly_totals_kwh", Real[]) if !isempty(monthly_totals_kwh) if length(monthly_totals_kwh != 12) - throw(@error "monthly_totals_kwh must contain a value for each of the 12 months") + throw(@error("monthly_totals_kwh must contain a value for each of the 12 months")) end bad_index = [] for (i, kwh) in enumerate(monthly_totals_kwh) @@ -127,7 +127,7 @@ function simulated_load(d::Dict) end end if !isempty(bad_index) - throw(@error "monthly_totals_kwh must contain a value for each month, and it is null for these months: $bad_index") + throw(@error("monthly_totals_kwh must contain a value for each month, and it is null for these months: $bad_index")) end end @@ -212,10 +212,10 @@ function simulated_load(d::Dict) end end if !isempty(error_list) - throw(@error "Invalid key(s) $error_list for load_type=heating") + throw(@error("Invalid key(s) $error_list for load_type=heating")) end if isnothing(doe_reference_name) - throw(@error "Please supply a doe_reference_name and optional scaling parameters (annual_mmbtu or monthly_mmbtu).") + throw(@error("Please supply a doe_reference_name and optional scaling parameters (annual_mmbtu or monthly_mmbtu).")) end # Annual loads (default is nothing) annual_mmbtu = get(d, "annual_mmbtu", nothing) @@ -223,7 +223,7 @@ function simulated_load(d::Dict) monthly_mmbtu = get(d, "monthly_mmbtu", Real[]) if !isempty(monthly_mmbtu) if length(monthly_mmbtu != 12) - throw(@error "monthly_mmbtu must contain a value for each of the 12 months") + throw(@error("monthly_mmbtu must contain a value for each of the 12 months")) end bad_index = [] for (i, mmbtu) in enumerate(monthly_mmbtu) @@ -232,14 +232,14 @@ function simulated_load(d::Dict) end end if !isempty(bad_index) - throw(@error "monthly_mmbtu must contain a value for each month, and it is null for these months: $bad_index") + throw(@error("monthly_mmbtu must contain a value for each month, and it is null for these months: $bad_index")) end end # Addressable heating load (default is 1.0) addressable_load_fraction = get(d, "addressable_load_fraction", 1.0) if typeof(addressable_load_fraction) <: Vector{} if length(addressable_load_fraction != 12) - throw(@error "addressable_load_fraction must contain a value for each of the 12 months") + throw(@error("addressable_load_fraction must contain a value for each of the 12 months")) end bad_index = [] for (i, frac) in enumerate(addressable_load_fraction) @@ -248,10 +248,10 @@ function simulated_load(d::Dict) end end if length(bad_index) > 0 - throw(@error "addressable_load_fraction must contain a value for each month, and it is null for these months: $bad_index") + throw(@error("addressable_load_fraction must contain a value for each month, and it is null for these months: $bad_index")) end elseif addressable_load_fraction < 0.0 ||addressable_load_fraction > 1.0 - throw(@error "addressable_load_fraction must be between 0.0 and 1.0") + throw(@error("addressable_load_fraction must be between 0.0 and 1.0")) end heating_load_inputs = Dict{Symbol, Any}() @@ -341,10 +341,10 @@ function simulated_load(d::Dict) end end if !isempty(error_list) - throw(@error "Invalid key(s) $error_list for load_type=cooling") + throw(@error("Invalid key(s) $error_list for load_type=cooling")) end if isnothing(doe_reference_name) - throw(@error "Please supply a doe_reference_name and optional scaling parameters (annual_tonhour or monthly_tonhour).") + throw(@error("Please supply a doe_reference_name and optional scaling parameters (annual_tonhour or monthly_tonhour).")) end # First check if one of the "fraction" inputs were given, which supersedes doe_reference_name @@ -364,7 +364,7 @@ function simulated_load(d::Dict) if !isempty(monthly_fraction) if length(monthly_fraction) > 1 if length(monthly_fraction != 12) - throw(@error "monthly_fraction must contain a value for each of the 12 months") + throw(@error("monthly_fraction must contain a value for each of the 12 months")) end bad_index = [] for (i, frac) in enumerate(monthly_fraction) @@ -373,7 +373,7 @@ function simulated_load(d::Dict) end end if length(bad_index) > 0 - throw(@error "monthly_fraction must contain a value between 0-1 for each month, and it is not valid for these months: $bad_index") + throw(@error("monthly_fraction must contain a value between 0-1 for each month, and it is not valid for these months: $bad_index")) end end days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # Only relevant for 2017 the CRB year, not for leap year @@ -399,7 +399,7 @@ function simulated_load(d::Dict) monthly_tonhour = get(d, "monthly_tonhour", Real[]) if !isempty(monthly_tonhour) if length(monthly_tonhour != 12) - throw(@error "monthly_tonhour must contain a value for each of the 12 months") + throw(@error("monthly_tonhour must contain a value for each of the 12 months")) end bad_index = [] for (i, mmbtu) in enumerate(monthly_tonhour) @@ -408,12 +408,12 @@ function simulated_load(d::Dict) end end if !isempty(bad_index) - throw(@error "monthly_tonhour must contain a value for each month, and it is null for these months: $bad_index") + throw(@error("monthly_tonhour must contain a value for each month, and it is null for these months: $bad_index")) end end if isnothing(annual_tonhour) && isempty(monthly_tonhour) - throw(@error "Use load_type=electric to get cooling load for buildings with no annual_tonhour or monthly_tonhour input (response.cooling_defaults)") + throw(@error("Use load_type=electric to get cooling load for buildings with no annual_tonhour or monthly_tonhour input (response.cooling_defaults)")) end # Build dependent inputs for cooling load @@ -444,7 +444,7 @@ function simulated_load(d::Dict) ]) return response else - throw(@error "Please supply a doe_reference_name and optional scaling parameters (annual_tonhour or monthly_tonhour), or annual_fraction, or monthly_fraction.") + throw(@error("Please supply a doe_reference_name and optional scaling parameters (annual_tonhour or monthly_tonhour), or annual_fraction, or monthly_fraction.")) end end end diff --git a/src/core/site.jl b/src/core/site.jl index bd11356e2..11af01fef 100644 --- a/src/core/site.jl +++ b/src/core/site.jl @@ -97,7 +97,7 @@ mutable struct Site end end if length(invalid_args) > 0 - error("Invalid argument values: $(invalid_args)") + throw(@error("Invalid Site argument values: $(invalid_args)")) end new(latitude, longitude, land_acres, roof_squarefeet, min_resil_time_steps, diff --git a/src/core/steam_turbine.jl b/src/core/steam_turbine.jl index 361d9a93e..587ff1917 100644 --- a/src/core/steam_turbine.jl +++ b/src/core/steam_turbine.jl @@ -181,7 +181,7 @@ function assign_st_elec_and_therm_prod_ratios!(st::SteamTurbine) # Check if the outlet steam vapor fraction is lower than the lowest allowable (-1 means superheated so no problem) if x_out != -1.0 && x_out < st.outlet_steam_min_vapor_fraction - error("The calculated steam outlet vapor fraction of $x_out is lower than the minimum allowable value of $(st.outlet_steam_min_vapor_fraction)") + throw(@error("The calculated steam outlet vapor fraction of $x_out is lower than the minimum allowable value of $(st.outlet_steam_min_vapor_fraction)")) end # Steam turbine shaft power calculations from enthalpy difference at inlet and outlet, and net power with efficiencies @@ -228,12 +228,12 @@ function get_steam_turbine_defaults_size_class(;avg_boiler_fuel_load_mmbtu_per_h n_classes = length(class_bounds) if !isnothing(size_class) if size_class < 1 || size_class > n_classes - @error "Invalid size_class given for steam_turbine, must be in [1,2,3,4]" + throw(@error("Invalid size_class given for steam_turbine, must be in [1,2,3,4]")) end end if !isnothing(avg_boiler_fuel_load_mmbtu_per_hour) if avg_boiler_fuel_load_mmbtu_per_hour <= 0 - @error "avg_boiler_fuel_load_mmbtu_per_hour must be > 0.0 MMBtu/hr" + throw(@error("avg_boiler_fuel_load_mmbtu_per_hour must be > 0.0 MMBtu/hr")) end steam_turbine_electric_efficiency = 0.07 # Typical, steam_turbine_kwe / boiler_fuel_kwt thermal_power_in_kw = avg_boiler_fuel_load_mmbtu_per_hour * KWH_PER_MMBTU diff --git a/src/core/techs.jl b/src/core/techs.jl index 27413c008..2bb039588 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -105,9 +105,10 @@ end Create a `Techs` struct for the REoptInputs. """ function Techs(s::Scenario) + #TODO: refactor code duplicated in Tech(s::MPCScenario) pvtechs = String[pv.name for pv in s.pvs] if length(Base.Set(pvtechs)) != length(pvtechs) - error("PV names must be unique, got $(pvtechs)") + throw(@error("PV names must be unique, got $(pvtechs)")) end all_techs = copy(pvtechs) @@ -233,7 +234,7 @@ Create a `Techs` struct for the MPCInputs function Techs(s::MPCScenario) pvtechs = String[pv.name for pv in s.pvs] if length(Base.Set(pvtechs)) != length(pvtechs) - error("PV names must be unique, got $(pvtechs)") + throw(@error("PV names must be unique, got $(pvtechs)")) end all_techs = copy(pvtechs) diff --git a/src/core/urdb.jl b/src/core/urdb.jl index 241598d06..87755f368 100644 --- a/src/core/urdb.jl +++ b/src/core/urdb.jl @@ -50,9 +50,9 @@ struct URDBrate tou_demand_rates::Array{Float64,2} # ratchet X tier tou_demand_ratchet_time_steps::Array{Array{Int64,1},1} # length = n_tou_demand_ratchets - demand_lookback_months::AbstractArray{Int,1} - demand_lookback_percent::Float64 - demand_lookback_range::Int + demand_lookback_months::AbstractArray{Int,1} # Array of 12 binary values, indicating months in which lookbackPercent applies. If any of these is true, lookbackRange should be zero. + demand_lookback_percent::Float64 # Lookback percentage. Applies to either lookbackMonths with value=1, or a lookbackRange. + demand_lookback_range::Int # Number of months for which lookbackPercent applies. If not 0, lookbackMonths values should all be 0. fixed_monthly_charge::Float64 annual_min_charge::Float64 @@ -98,7 +98,7 @@ function URDBrate(urdb_response::Dict, year::Int=2019; time_steps_per_hour=1) n_monthly_demand_tiers, monthly_demand_tier_limits, monthly_demand_rates, n_tou_demand_tiers, tou_demand_tier_limits, tou_demand_rates, tou_demand_ratchet_time_steps = - parse_demand_rates(urdb_response, year) + parse_demand_rates(urdb_response, year, time_steps_per_hour=time_steps_per_hour) energy_rates, energy_tier_limits, n_energy_tiers, sell_rates = parse_urdb_energy_costs(urdb_response, year; time_steps_per_hour=time_steps_per_hour) @@ -134,7 +134,7 @@ function URDBrate(urdb_response::Dict, year::Int=2019; time_steps_per_hour=1) ) end - +#TODO: refactor two download_urdb to reduce duplicated code function download_urdb(urdb_label::String; version::Int=8) url = string("https://api.openei.org/utility_rates", "?api_key=", urdb_key, "&version=", version , "&format=json", "&detail=full", @@ -146,20 +146,20 @@ function download_urdb(urdb_label::String; version::Int=8) r = HTTP.get(url, require_ssl_verification=false) # cannot verify on NREL VPN response = JSON.parse(String(r.body)) if r.status != 200 - error("Bad response from URDB: $(response["errors"])") # TODO URDB has "errors"? + throw(@error("Bad response from URDB: $(response["errors"])")) # TODO URDB has "errors"? end catch e - error("Error occurred :$(e)") + throw(@error("Error occurred :$(e)")) end rates = response["items"] # response['items'] contains a vector of dicts if length(rates) == 0 - error("Could not find $(urdb_label) in URDB.") + throw(@error("Could not find $(urdb_label) in URDB.")) end if rates[1]["label"] == urdb_label return rates[1] else - error("Could not find $(urdb_label) in URDB.") + throw(@error("Could not find $(urdb_label) in URDB.")) end end @@ -175,15 +175,15 @@ function download_urdb(util_name::String, rate_name::String; version::Int=8) r = HTTP.get(url, require_ssl_verification=false) # cannot verify on NREL VPN response = JSON.parse(String(r.body)) if r.status != 200 - error("Bad response from URDB: $(response["errors"])") # TODO URDB has "errors"? + throw(@error("Bad response from URDB: $(response["errors"])")) # TODO URDB has "errors"? end catch e - error("Error occurred :$(e)") + throw(@error("Error occurred :$(e)")) end rates = response["items"] # response['items'] contains a vector of dicts if length(rates) == 0 - error("Could not find $(rate_name) in URDB.") + throw(@error("Could not find $(rate_name) in URDB.")) end matched_rates = [] @@ -205,7 +205,7 @@ function download_urdb(util_name::String, rate_name::String; version::Int=8) end if length(matched_rates) == 0 - error("Could not find $(rate_name) in URDB.") + throw(@error("Could not find $(rate_name) in URDB.")) end return matched_rates[newest_index] @@ -223,7 +223,7 @@ use URDB dict to return rates, energy_cost_vector, energy_tier_limits_kwh where: """ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM = 1.0e8) if length(d["energyratestructure"]) == 0 - error("No energyratestructure in URDB response.") + throw(@error("No energyratestructure in URDB response.")) end # TODO check bigM (in multiple functions) energy_tiers = Float64[] @@ -232,7 +232,7 @@ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM end energy_tier_set = Set(energy_tiers) if length(energy_tier_set) > 1 - @warn "energy periods contain different numbers of tiers, using limits of period with most tiers" + @warn "Energy periods contain different numbers of tiers, using limits of period with most tiers." end period_with_max_tiers = findall(energy_tiers .== maximum(energy_tiers))[1] n_energy_tiers = Int(maximum(energy_tier_set)) @@ -313,12 +313,12 @@ end """ - parse_demand_rates(d::Dict, year::Int; bigM=1.0e8) + parse_demand_rates(d::Dict, year::Int; bigM=1.0e8, time_steps_per_hour::Int) Parse monthly ("flat") and TOU demand rates can modify URDB dict when there is inconsistent numbers of tiers in rate structures """ -function parse_demand_rates(d::Dict, year::Int; bigM=1.0e8) +function parse_demand_rates(d::Dict, year::Int; bigM=1.0e8, time_steps_per_hour::Int) if haskey(d, "flatdemandstructure") scrub_urdb_demand_tiers!(d["flatdemandstructure"]) @@ -335,7 +335,7 @@ function parse_demand_rates(d::Dict, year::Int; bigM=1.0e8) scrub_urdb_demand_tiers!(d["demandratestructure"]) tou_demand_tier_limits = parse_urdb_demand_tiers(d["demandratestructure"]) n_tou_demand_tiers = length(tou_demand_tier_limits) - ratchet_time_steps, tou_demand_rates = parse_urdb_tou_demand(d, year=year, n_tiers=n_tou_demand_tiers) + ratchet_time_steps, tou_demand_rates = parse_urdb_tou_demand(d, year=year, n_tiers=n_tou_demand_tiers, time_steps_per_hour=time_steps_per_hour) else tou_demand_tier_limits = [] n_tou_demand_tiers = 0 @@ -363,8 +363,7 @@ function scrub_urdb_demand_tiers!(A::Array) n_tiers = maximum(len_tiers_set) if length(len_tiers_set) > 1 - @warn """Demand rate structure has varying number of tiers in periods. - Making the number of tiers the same across all periods by repeating the last tier.""" + @warn "Demand rate structure has varying number of tiers in periods. Making the number of tiers the same across all periods by repeating the last tier." for (i, rate) in enumerate(A) n_tiers_in_period = length(rate) if n_tiers_in_period != n_tiers @@ -445,7 +444,7 @@ end return array of arrary for ratchet time steps, tou demand rates array{ratchet, tier} """ -function parse_urdb_tou_demand(d::Dict; year::Int, n_tiers::Int) +function parse_urdb_tou_demand(d::Dict; year::Int, n_tiers::Int, time_steps_per_hour::Int) if !haskey(d, "demandratestructure") return [], [] end @@ -456,7 +455,7 @@ function parse_urdb_tou_demand(d::Dict; year::Int, n_tiers::Int) for month in range(1, stop=12) for period in range(0, stop=n_periods) - time_steps = get_tou_demand_steps(d, year=year, month=month, period=period-1) + time_steps = get_tou_demand_steps(d, year=year, month=month, period=period-1, time_steps_per_hour=time_steps_per_hour) if length(time_steps) > 0 # can be zero! not every month contains same number of periods n_ratchets += 1 append!(ratchet_time_steps, [time_steps]) @@ -478,10 +477,6 @@ end return Array{Int, 1} for time_steps in ratchet (aka period) """ function get_tou_demand_steps(d::Dict; year::Int, month::Int, period::Int, time_steps_per_hour=1) - step_array = Int[] - start_step = 1 - start_hour = 1 - if month > 1 plus_days = 0 for m in range(1, stop=month-1) @@ -490,25 +485,27 @@ function get_tou_demand_steps(d::Dict; year::Int, month::Int, period::Int, time_ plus_days -= 1 end end - start_hour += plus_days * 24 - start_step = start_hour * time_steps_per_hour + start_hour = 1 + plus_days * 24 + start_step = 1 + plus_days * 24 * time_steps_per_hour + else + start_hour = 1 + start_step = 1 end - hour_of_year = start_hour step_of_year = start_step + step_array = Int[] for day in range(1, stop=daysinmonth(Date(string(year) * "-" * string(month)))) for hour in range(1, stop=24) - if dayofweek(Date(year, month, day)) < 6 && - d["demandweekdayschedule"][month][hour] == period - append!(step_array, step_of_year) - elseif dayofweek(Date(year, month, day)) > 5 && - d["demandweekendschedule"][month][hour] == period - append!(step_array, step_of_year) + if (dayofweek(Date(year, month, day)) < 6 && + d["demandweekdayschedule"][month][hour] == period) || + (dayofweek(Date(year, month, day)) > 5 && + d["demandweekendschedule"][month][hour] == period) + + append!(step_array, collect(step_of_year:step_of_year+time_steps_per_hour-1)) end - step_of_year += 1 + step_of_year += time_steps_per_hour end - hour_of_year += 1 end return step_array end @@ -568,20 +565,18 @@ URDB lookback fields: - Lookback percentage. Applies to either lookbackMonths with value=1, or a lookbackRange. - lookbackRange - Type: integer - - Number of months for which lookbackPercent applies. If not 0, lookbackMonths values should all be 0. + - Number of previous months for which lookbackPercent applies each month. If not 0, lookbackMonths values should all be 0. """ function parse_urdb_lookback_charges(d::Dict) - lookback_months = get(d, "lookbackMonths", Int[]) - lookback_percent = Float64(get(d, "lookbackPercent", 0.0)) - lookback_range = Int64(get(d, "lookbackRange", 0.0)) - - reopt_lookback_months = Int[] - if lookback_range != 0 && length(lookback_months) == 12 - for mth in range(1, stop=12) - if lookback_months[mth] == 1 - push!(reopt_lookback_months, mth) - end - end + lookback_months = get(d, "lookbackmonths", Int[]) + lookback_percent = Float64(get(d, "lookbackpercent", 0.0)) + lookback_range = Int64(get(d, "lookbackrange", 0.0)) + + if lookback_range == 0 && length(lookback_months) == 12 + lookback_months = collect(1:12)[lookback_months .== 1] + elseif lookback_range !=0 && length(lookback_months) == 12 + throw(@warn("URDB rate contains both lookbackRange and lookbackMonths. Only lookbackRange will apply.")) end - return reopt_lookback_months, lookback_percent, lookback_range + + return lookback_months, lookback_percent, lookback_range end \ No newline at end of file diff --git a/src/core/utils.jl b/src/core/utils.jl index ae633687e..ba53b52db 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -174,9 +174,9 @@ function dictkeys_tosymbols(d::Dict) "production_factor_series", "monthly_energy_rates", "monthly_demand_rates", "blended_doe_reference_percents", - "coincident_peak_load_charge_per_kw", "fuel_cost_per_mmbtu", + "coincident_peak_load_charge_per_kw", "grid_draw_limit_kw_by_time_step", "export_limit_kw_by_time_step", - "outage_probabilities", "wholesale_rate", + "outage_probabilities", "emissions_factor_series_lb_CO2_per_kwh", "emissions_factor_series_lb_NOx_per_kwh", "emissions_factor_series_lb_SO2_per_kwh", @@ -185,7 +185,7 @@ function dictkeys_tosymbols(d::Dict) try v = convert(Array{Real, 1}, v) catch - @debug "Unable to convert $k to an Array{Real, 1}" + throw(@error("Unable to convert $k to an Array{Real, 1}")) end end if k in [ @@ -194,7 +194,7 @@ function dictkeys_tosymbols(d::Dict) try v = convert(Array{String, 1}, v) catch - @warn "Unable to convert $k to an Array{String, 1}" + throw(@error("Unable to convert $k to an Array{String, 1}")) end end if k in [ @@ -203,7 +203,7 @@ function dictkeys_tosymbols(d::Dict) try v = convert(Vector{Vector{Int64}}, v) catch - @debug "Unable to convert $k to a Vector{Vector{Int64}}" + throw(@error("Unable to convert $k to a Vector{Vector{Int64}}")) end end if k in [ @@ -212,7 +212,21 @@ function dictkeys_tosymbols(d::Dict) try v = convert(Array{Int64, 1}, v) catch - @warn "Unable to convert $k to a Array{Int64, 1}" + throw(@error("Unable to convert $k to a Array{Int64, 1}")) + end + end + if k in [ + "fuel_cost_per_mmbtu", "wholesale_rate" + ] && !isnothing(v) + #if not a Real try to convert to an Array{Real} + if !(typeof(v) <: Real) + try + if typeof(v) <: Array + v = convert(Array{Real, 1}, v) + end + catch + throw(@error("Unable to convert $k to a Array{Real, 1} or Real")) + end end end d2[Symbol(k)] = v @@ -275,7 +289,7 @@ function per_hour_value_to_time_series(x::AbstractVector{<:Real}, time_steps_per end return vals end - @error "Cannot convert $name to appropriate length time series." + throw(@error("Cannot convert $name to appropriate length time series.")) end """ @@ -316,11 +330,11 @@ function generate_year_profile_hourly(year::Int64, consecutive_periods::Abstract start_date = Dates.firstdayofweek(start_date_of_month_year) + Dates.Week(start_week_of_month - 1) + Dates.Day(start_day_of_week - 1) # Throw an error if start_date is in the previous month when start_week_of_month=1 and there is no start_day_of_week in the first week of the month. if Dates.month(start_date) != start_month - @error "For $(error_start_text), there is no day $(start_day_of_week) ($(day_of_week_name[start_day_of_week])) in the first week of month $(start_month) ($(Dates.monthname(start_date))), $(year)" + throw(@error("For $(error_start_text), there is no day $(start_day_of_week) ($(day_of_week_name[start_day_of_week])) in the first week of month $(start_month) ($(Dates.monthname(start_date))), $(year)")) end start_datetime = Dates.DateTime(start_date) + Dates.Hour(start_hour - 1) if Dates.year(start_datetime + Dates.Hour(duration_hours)) > year - @error "For $(error_start_text), the start day/time and duration_hours exceeds the end of the year. Please specify two separate unavailability periods: one for the beginning of the year and one for up to the end of the year." + throw(@error("For $(error_start_text), the start day/time and duration_hours exceeds the end of the year. Please specify two separate unavailability periods: one for the beginning of the year and one for up to the end of the year.")) else #end_datetime is the last hour that is 1.0 (e.g. that is still unavailable), not the first hour that is 0.0 after the period end_datetime = start_datetime + Dates.Hour(duration_hours - 1) @@ -347,16 +361,16 @@ function get_ambient_temperature(latitude::Real, longitude::Real; timeframe="hou r = HTTP.get(url) response = JSON.parse(String(r.body)) if r.status != 200 - error("Bad response from PVWatts: $(response["errors"])") + throw(@error("Bad response from PVWatts: $(response["errors"])")) end @info "PVWatts success." tamb = collect(get(response["outputs"], "tamb", [])) # Celcius if length(tamb) != 8760 - @error "PVWatts did not return a valid temperature. Got $tamb" + throw(@error("PVWatts did not return a valid temperature. Got $tamb")) end return tamb catch e - @error "Error occurred when calling PVWatts: $e" + throw(@error("Error occurred when calling PVWatts: $e")) end end @@ -374,16 +388,16 @@ function get_pvwatts_prodfactor(latitude::Real, longitude::Real; timeframe="hour r = HTTP.get(url) response = JSON.parse(String(r.body)) if r.status != 200 - error("Bad response from PVWatts: $(response["errors"])") + throw(@error("Bad response from PVWatts: $(response["errors"])")) end @info "PVWatts success." watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W) if length(watts) != 8760 - @error "PVWatts did not return a valid production_factor. Got $watts" + throw(@error("PVWatts did not return a valid prodfactor. Got $watts")) end return watts catch e - @error "Error occurred when calling PVWatts: $e" + throw(@error("Error occurred when calling PVWatts: $e")) end end diff --git a/src/core/wind.jl b/src/core/wind.jl index 06d09199c..a34703161 100644 --- a/src/core/wind.jl +++ b/src/core/wind.jl @@ -60,7 +60,7 @@ can_net_meter = true, can_wholesale = true, can_export_beyond_nem_limit = true - operating_reserve_required_fraction::Real = off_grid_flag ? 0.50 : 0.0, # Only applicable when off_grid_flag is True. Applied to each time_step as a % of wind generation serving load. + operating_reserve_required_fraction::Real = off_grid_flag ? 0.50 : 0.0, # Only applicable when `off_grid_flag` is true. Applied to each time_step as a % of wind generation serving load. ``` `size_class` must be one of ["residential", "commercial", "medium", "large"]. If `size_class` is not provided then it is determined based on the average electric load. @@ -164,7 +164,7 @@ struct Wind <: AbstractTech can_export_beyond_nem_limit = off_grid_flag ? false : true, can_curtail= true, average_elec_load = 0.0, - operating_reserve_required_fraction::Real = off_grid_flag ? 0.50 : 0.0, # Only applicable when off_grid_flag is True. Applied to each time_step as a % of wind generation serving load. + operating_reserve_required_fraction::Real = off_grid_flag ? 0.50 : 0.0, # Only applicable when `off_grid_flag` is true. Applied to each time_step as a % of wind generation serving load. ) size_class_to_hub_height = Dict( "residential"=> 20, @@ -197,7 +197,7 @@ struct Wind <: AbstractTech size_class = "large" end elseif !(size_class in keys(size_class_to_hub_height)) - @error "Wind.size_class must be one of $(keys(size_class_to_hub_height))" + throw(@error("Wind size_class must be one of $(keys(size_class_to_hub_height))")) end if isnothing(installed_cost_per_kw) @@ -211,12 +211,12 @@ struct Wind <: AbstractTech hub_height = size_class_to_hub_height[size_class] if !(off_grid_flag) && !(operating_reserve_required_fraction == 0.0) - @warn "Wind operating_reserve_required_fraction applies only when off_grid_flag is True. Setting operating_reserve_required_fraction to 0.0 for this on-grid analysis." + @warn "Wind operating_reserve_required_fraction applies only when `off_grid_flag` is true. Setting operating_reserve_required_fraction to 0.0 for this on-grid analysis." operating_reserve_required_fraction = 0.0 end if off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) - @warn "Net metering, wholesale, and grid exports are not possible for off-grid scenarios. Setting Wind can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False." + @warn "Setting Wind can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." can_net_meter = false can_wholesale = false can_export_beyond_nem_limit = false diff --git a/src/logging.jl b/src/logging.jl new file mode 100644 index 000000000..9f3a4de46 --- /dev/null +++ b/src/logging.jl @@ -0,0 +1,203 @@ +# ********************************************************************************* +# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# Redistributions of source code must retain the above copyright notice, this list +# of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, this +# list of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +# ********************************************************************************* + +""" + REoptLogger + +- Instantiates a global logger of type REoptLogger which can log to console for logging events >= `@info` and to a dictionary for events >= `@warn`. + Dictionary collects information in the following format: + - Warn + - Keys for file names with warnings + - Values of messages associated with those warnings + - Error + - Keys for file names with errors + - Values of messages associated with those errors + +- The logger is instantiated in REopt.Scenario, REopt.REoptInputs functions and a few instances of REopt.run_reopt functions + +- Messages are appended to the results dictionary using the following layout: + - warnings (array of Tuples) + - 1st element: Folder, file and line of error + - 2nd element: Warning text + - errors (array of Tuples) + - 1st element: Folder, file and line of error + - 2nd element: Error text with stacktrace if uncaught error + +- Logger dictionary is flushed at every new REopt run by checking if logger type is REoptLogger +""" +struct REoptLogger <: Logging.AbstractLogger + d::Dict + io::IO +end + +REoptLogger() = REoptLogger(Dict(), stderr) + +Logging.min_enabled_level(logger::REoptLogger) = Logging.Info + +function Logging.shouldlog(logger::REoptLogger, level, _module, group, id) + return true + end + +Logging.catch_exceptions(logger::REoptLogger) = true + +function Logging.handle_message(logger::REoptLogger, lvl, msg, _mod, group, id, file, line; kwargs...) + + msglines = split(chomp(convert(String, string(msg))::String), '\n') + msg1, rest = Iterators.peel(msglines) + + col = nothing + if string(lvl)=="Error" + col = :red + elseif string(lvl)=="Warn" + col = :light_yellow + elseif string(lvl)=="Info" + col = :cyan + else + col = :default + end + + printstyled(logger.io, "┌ ", _mod, " | ", lvl, ": "; bold=true, color=col) + printstyled(logger.io, msg1, "\n") + for msg in rest + println(logger.io, "│ ", msg) + end + for (key, val) in kwargs + key === :maxlog && continue + println(logger.io, "│ ", key, " = ", val) + end + println(logger.io, "└ @ ", _mod, " ", file, ":", line) + + if string(lvl) in ["Warn","Error"] + if string(lvl) ∉ keys(logger.d) # key doesnt exists + logger.d[string(lvl)] = Dict() + end + + # Does the key for file exist? + if Sys.iswindows() #windows + splitter = "\\" + else # unix/mac + splitter = "/" + end + + splt = split(file, splitter) + f = join([splt[end-1], splt[end], line], "_") + + if f ∉ keys(logger.d[string(lvl)]) #file name doesnt exists + logger.d[string(lvl)][f] = Any[] + end + + push!(logger.d[string(lvl)][f], msg) + end +end + +""" + handle_errors(e::E, stacktrace::V) where { + E <: Exception, + V <: Vector + } + +Creates a results dictionary in case of an error from REopt.jl with Warnings and Errors from logREopt.d. The unhandled error+stacktrace is returned to the user. +""" +function handle_errors(e::E, stacktrace::V) where { + E <: Exception, + V <: Vector + } + + results = Dict( + "Messages"=>Dict(), + "status"=>"error" + ) + + results["Messages"] = logger_to_dict() + + push!(results["Messages"]["errors"], (string(e),string.(stacktrace))) + return results +end + +""" + handle_errors() + +Creates a results dictionary in case of a handled error from REopt.jl with Warnings and Errors from logREopt.d, which is returned to the user. +""" +function handle_errors() + + results = Dict( + "Messages"=>Dict(), + "status"=>"error" + ) + + results["Messages"] = logger_to_dict() + + return results +end + +""" + logger_to_dict() + +The purpose of this function is to extract warnings and errors from REopt logger and package them in a dictionary which can be returned to the user as-is. +""" +function logger_to_dict() + + d = Dict() + d["warnings"] = [] + d["errors"] = [] + + if "Warn" in keys(logREopt.d) + for (keys,values) in logREopt.d["Warn"] + push!(d["warnings"], (keys, values)) + end + end + + if "Error" in keys(logREopt.d) + for (keys,values) in logREopt.d["Error"] + push!(d["errors"], (keys, values)) + end + end + + return d +end + +""" + instantiate_logger() + +Instantiate a global logger of type REoptLogger and set it to global logger for downstream processing. +""" +function instantiate_logger() + + if !isa(global_logger(), REoptLogger) + global logREopt = REoptLogger() + global_logger(logREopt) + @debug "Created custom REopt Logger" + else + @debug "Already REoptLogger" + logREopt.d["Warn"] = Dict() + logREopt.d["Error"] = Dict() + end +end \ No newline at end of file diff --git a/src/mpc/model.jl b/src/mpc/model.jl index 45bf9ece7..5fec5c006 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -138,7 +138,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) elseif b in p.s.storage.types.cold add_cold_thermal_storage_dispatch_constraints(m, p, b) else - @error("Invalid storage does not fall in a thermal or electrical set") + throw(@error("Invalid storage does not fall in a thermal or electrical set")) end end end diff --git a/src/mpc/scenario.jl b/src/mpc/scenario.jl index 454a1edfd..b1dd5d7ec 100644 --- a/src/mpc/scenario.jl +++ b/src/mpc/scenario.jl @@ -91,7 +91,7 @@ function MPCScenario(d::Dict) elseif typeof(d["PV"]) <: AbstractDict push!(pvs, MPCPV(;dictkeys_tosymbols(d["PV"])...)) else - error("PV input must be Dict or Dict[].") + throw(@error("PV input must be Dict or Dict[].")) end end diff --git a/test/scenarios/logger.json b/test/scenarios/logger.json new file mode 100644 index 000000000..49dcb84d6 --- /dev/null +++ b/test/scenarios/logger.json @@ -0,0 +1,13 @@ +{ + "ElectricTariff": { + "urdb_label": "632b4e5279d29ba1130491d11" + }, + "Site": { + "latitude": 65.0, + "longitude": -155.8394336 + }, + "ElectricLoad": { + "annual_kwh": 100000.0, + "doe_reference_name": "MidriceApartment" + } +} \ No newline at end of file diff --git a/test/scenarios/lookback_rate.json b/test/scenarios/lookback_rate.json index 7f8364c30..566eaa319 100644 --- a/test/scenarios/lookback_rate.json +++ b/test/scenarios/lookback_rate.json @@ -4,14 +4,21 @@ "latitude": 33.748995 }, "ElectricLoad": { - "doe_reference_name": "MidriseApartment", - "annual_kwh": 24000.0 }, "ElectricTariff": { "urdb_label": "539f6a23ec4f024411ec8bf9" }, "Financial": { - "offtaker_tax_rate_fraction": 0.26, - "elec_cost_escalation_rate_fraction": 0.026 + "offtaker_discount_rate_fraction": 0.0, + "offtaker_tax_rate_fraction": 0.0, + "elec_cost_escalation_rate_fraction": 0.0, + "om_cost_escalation_rate_fraction": 0.0 + }, + "PV": { + "max_kw": 0.0 + }, + "Storage": { + "max_kw": 0.0, + "max_kwh": 0.0 } } \ No newline at end of file diff --git a/test/test_with_cplex.jl b/test/test_with_cplex.jl index 489f7f860..d908b4d6c 100644 --- a/test/test_with_cplex.jl +++ b/test/test_with_cplex.jl @@ -149,6 +149,48 @@ end urdb_label = "59bc22705457a3372642da67" # tiered monthly demand rate end +@testset "Lookback Demand Charges" begin + # 1. Testing custom rate from user with demand_lookback_months + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["demand_lookback_months"] = [1,0,0,1,0,0,0,0,0,0,0,1] # Jan, April, Dec + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + + m = Model(optimizer_with_attributes(CPLEX.Optimizer, "CPX_PARAM_SCRIND" => 0)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [300,300,300,400,300,500,300,300,300,300,300,300] # 300 = 400*0.75. Sets peak in all months excpet April and June + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + + # 2. Testing custom rate from user with demand_lookback_range + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + d["ElectricTariff"]["demand_lookback_range"] = 6 + + m = Model(optimizer_with_attributes(CPLEX.Optimizer, "CPX_PARAM_SCRIND" => 0)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [225, 225, 225, 400, 300, 500, 375, 375, 375, 375, 375, 375] + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost +end + ## equivalent REopt API Post for test 2: # NOTE have to hack in API levelization_factor to get LCC within 5e-5 (Mosel tol) # {"Scenario": { diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 2c9ba1112..66d031f02 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -551,9 +551,59 @@ end end @testset "Lookback Demand Charges" begin + # 1. Testing rate from URDB + data = JSON.parsefile("./scenarios/lookback_rate.json") + # urdb_label used https://apps.openei.org/IURDB/rate/view/539f6a23ec4f024411ec8bf9#2__Demand + # has a demand charge lookback of 35% for all months with 2 different demand charges based on which month + data["ElectricLoad"]["loads_kw"] = ones(8760) + data["ElectricLoad"]["loads_kw"][8] = 100.0 + inputs = REoptInputs(Scenario(data)) m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) - results = run_reopt(m, "./scenarios/lookback_rate.json") - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 721.99 + results = run_reopt(m, inputs) + # Expected result is 100 kW demand for January, 35% of that for all other months and + # with 5x other $10.5/kW cold months and 6x $11.5/kW warm months + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 100 * (10.5 + 0.35*10.5*5 + 0.35*11.5*6) + + # 2. Testing custom rate from user with demand_lookback_months + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["demand_lookback_months"] = [1,0,0,1,0,0,0,0,0,0,0,1] # Jan, April, Dec + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [300,300,300,400,300,500,300,300,300,300,300,300] # 300 = 400*0.75. Sets peak in all months excpet April and June + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + + # 3. Testing custom rate from user with demand_lookback_range + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + d["ElectricTariff"]["demand_lookback_range"] = 6 + + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [225, 225, 225, 400, 300, 500, 375, 375, 375, 375, 375, 375] + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + end @testset "Blended tariff" begin @@ -1482,3 +1532,96 @@ end end end +@testset "Custom REopt logger" begin + + # Throw a handled error + d = JSON.parsefile("./scenarios/logger.json") + + m1 = Model(Xpress.Optimizer) + m2 = Model(Xpress.Optimizer) + r = run_reopt([m1,m2], d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + m = Model(Xpress.Optimizer) + r = run_reopt(m, d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Type is dict when errors, otherwise type REoptInputs + @test isa(REoptInputs(d), Dict) + + # Using filepath + n1 = Model(Xpress.Optimizer) + n2 = Model(Xpress.Optimizer) + r = run_reopt([n1,n2], "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + n = Model(Xpress.Optimizer) + r = run_reopt(n, "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Throw an unhandled error: Bad URDB rate -> stack gets returned for debugging + d["ElectricLoad"]["doe_reference_name"] = "MidriseApartment" + d["ElectricTariff"]["urdb_label"] = "62c70a6c40a0c425535d387b" + + m1 = Model(Xpress.Optimizer) + m2 = Model(Xpress.Optimizer) + r = run_reopt([m1,m2], d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + m = Model(Xpress.Optimizer) + r = run_reopt(m, d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Type is dict when errors, otherwise type REoptInputs + @test isa(REoptInputs(d), Dict) + + # Using filepath + n1 = Model(Xpress.Optimizer) + n2 = Model(Xpress.Optimizer) + r = run_reopt([n1,n2], "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + n = Model(Xpress.Optimizer) + r = run_reopt(n, "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 +end