Skip to content

Commit

Permalink
Merge pull request #374 from NREL/develop
Browse files Browse the repository at this point in the history
 4 bug fix PRs
  • Loading branch information
hdunham authored Apr 22, 2024
2 parents 9cd7fdf + d19f9b3 commit d7b888a
Show file tree
Hide file tree
Showing 20 changed files with 78 additions and 56 deletions.
20 changes: 14 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## Develop - 2024-04-11
### Fixed
- Added `export_rate_beyond_net_metering_limit` to list of inputs to be converted to type Real, to avoid MethodError if type is vector of Any.
- Fix blended CRB processing when one or more load types have zero annual energy
- When calculating CHP fuel intercept and slope, use 1 for the HHV because CHP fuel measured in units of kWh, instead of using non-existent **CHP.fuel_higher_heating_value_kwh_per_gal**
- Changed instances of indexing using i in 1:length() paradigm to use eachindex() or axes() instead because this is more robust
- In `src/core/urdb.jl`, ensure values from the "energyweekdayschedule" and "energyweekendschedule" arrays in the URDB response dictionary are converted to _Int_ before being used as indices
- Handle an array of length 1 for CHP.installed_cost_per_kw which fixes the API using this parameter
### Changed
- add **ElectricStorage** input option **soc_min_applies_during_outages** (which defaults to _false_) and only apply the minimum state of charge constraint in function `add_MG_storage_dispatch_constraints` if it is _true_
- Renamed function `generator_fuel_slope_and_intercept` to `fuel_slope_and_intercept` and generalize to not be specific to diesel measured in units of gal, then use for calculating non diesel fuel slope and intercept too

## v0.44.0
### Added
- in `src/settings.jl`, added new const **INDICATOR_COMPATIBLE_SOLVERS**
Expand All @@ -40,7 +52,6 @@ Classify the change according to the following categories:
## v0.42.0
### Changed
- In `core/pv.jl` a change was made to make sure we are using the same assumptions as PVWatts guidelines, the default `tilt` angle for a fixed array should be 20 degrees, irrespective of it being a rooftop `(1)` or ground-mounted (open-rack)`(2)` system. By default the `tilt` will be set to 20 degrees for ground-mount and rooftop, and 0 degrees for axis-tracking (`array_type = (3) or (4)`)

> "The PVWatts® default value for the tilt angle depends on the array type: For a fixed array, the default value is 20 degrees, and for one-axis tracking the default value is zero. A common rule of thumb for fixed arrays is to set the tilt angle to the latitude of the system's location to maximize the system's total electrical output over the year. Use a lower tilt angle favor peak production in the summer months when the sun is high in the sky, or a higher tilt angle to increase output during winter months. Higher tilt angles tend to cost more for racking and mounting hardware, and may increase the risk of wind damage to the array."
## v0.41.0
Expand All @@ -57,13 +68,11 @@ Classify the change according to the following categories:
)
- Changed calculation of all `annual` emissions results (e.g. **Site.annual_emissions_tonnes_CO2**) to simple annual averages (lifecycle emissions divided by analysis_years). This is because the default climate emissions from Cambium are already levelized over the analysis horizon and therefore "year_one" emissions cannot be easily obtained.
- Changed name of exported function **emissions_profiles** to **avert_emissions_profiles**

### Added
- In `src/REopt.jl` and `src/electric_utility.jl`, added **cambium_emissions_profile** as an export for use via the REopt_API.
- In `src/REopt.jl`, added new const **EMISSIONS_DECREASE_DEFAULTS**
- In `src/results/electric_utility.jl` **cambium_emissions_region**
- In `test/runtests.jl` and `test/test_with_xpress.jl`, added testset **Cambium Emissions**

### Fixed
- Adjust grid emissions profiles for day of week alignment with load_year.
- In `test_with_xpress.jl`, updated "Emissions and Renewable Energy Percent" expected values to account for load year adjustment.
Expand All @@ -73,7 +82,6 @@ Classify the change according to the following categories:
## v0.40.0
### Changed
- Changed **macrs_bonus_fraction** to from 0.80 to 0.60 (60%) for CHP, ElectricStorage, ColdThermalStorage, HotThermalStorage GHP, PV, Wind

### Fixed
- In `reopt.jl`, group objective function incentives (into **ObjectivePenalties**) and avoid directly modifying m[:Costs]. Previously, some of these were incorrectly included in the reported **Financial.lcc**.

Expand Down Expand Up @@ -684,7 +692,7 @@ Other changes:
- handle missing input key for `year_one_soc_series_pct` in `outage_simulator`
- remove erroneous `total_unserved_load = 0` output
- `dvUnservedLoad` definition was allowing microgrid production to storage and curtailment to be double counted towards meeting critical load
#### Added
### Added
- add `unserved_load_per_outage` output

## v0.4.1
Expand Down Expand Up @@ -712,7 +720,7 @@ Other changes:
## v0.3.0
### Added
- add separate decision variables and constraints for microgrid tech capacities
- new Site input `mg_tech_sizes_equal_grid_sizes` (boolean), when `false` the microgrid tech capacities are constrained to be <= the grid connected tech capacities
- new Site input `mg_tech_sizes_equal_grid_sizes` (boolean), when _false_ the microgrid tech capacities are constrained to be <= the grid connected tech capacities
### Fixed
- allow non-integer `outage_probabilities`
- correct `total_unserved_load` output
Expand Down
9 changes: 5 additions & 4 deletions src/constraints/chp_constraints.jl
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE.
function add_chp_fuel_burn_constraints(m, p; _n="")
# Fuel burn slope and intercept
fuel_burn_full_load = 1.0 / p.s.chp.electric_efficiency_full_load # [kWt/kWe]
fuel_burn_half_load = 0.5 / p.s.chp.electric_efficiency_half_load # [kWt/kWe]
fuel_burn_slope = (fuel_burn_full_load - fuel_burn_half_load) / (1.0 - 0.5) # [kWt/kWe]
fuel_burn_intercept = fuel_burn_full_load - fuel_burn_slope * 1.0 # [kWt/kWe_rated]
fuel_burn_slope, fuel_burn_intercept = fuel_slope_and_intercept(;
electric_efficiency_full_load = p.s.chp.electric_efficiency_full_load,
electric_efficiency_half_load = p.s.chp.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_unit=1
)

# Fuel cost
m[:TotalCHPFuelCosts] = @expression(m,
Expand Down
4 changes: 2 additions & 2 deletions src/constraints/generator_constraints.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE.
function add_fuel_burn_constraints(m,p)
fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr = generator_fuel_slope_and_intercept(
fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr = fuel_slope_and_intercept(
electric_efficiency_full_load=p.s.generator.electric_efficiency_full_load,
electric_efficiency_half_load=p.s.generator.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_gal=p.s.generator.fuel_higher_heating_value_kwh_per_gal
fuel_higher_heating_value_kwh_per_unit=p.s.generator.fuel_higher_heating_value_kwh_per_gal
)
@constraint(m, [t in p.techs.gen, ts in p.time_steps],
m[:dvFuelUsage][t, ts] == (fuel_slope_gal_per_kwhe * p.s.generator.fuel_higher_heating_value_kwh_per_gal *
Expand Down
23 changes: 13 additions & 10 deletions src/constraints/outage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,10 @@ end


function add_MG_Gen_fuel_burn_constraints(m,p)
fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr = generator_fuel_slope_and_intercept(
fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr = fuel_slope_and_intercept(
electric_efficiency_full_load=p.s.generator.electric_efficiency_full_load,
electric_efficiency_half_load=p.s.generator.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_gal=p.s.generator.fuel_higher_heating_value_kwh_per_gal
fuel_higher_heating_value_kwh_per_unit=p.s.generator.fuel_higher_heating_value_kwh_per_gal
)
# Define dvMGFuelUsed by summing over outage time_steps.
@constraint(m, [t in p.techs.gen, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps],
Expand Down Expand Up @@ -174,10 +174,11 @@ end

function add_MG_CHP_fuel_burn_constraints(m, p; _n="")
# Fuel burn slope and intercept
fuel_burn_full_load = 1.0 / p.s.chp.electric_efficiency_full_load # [kWt/kWe]
fuel_burn_half_load = 0.5 / p.s.chp.electric_efficiency_half_load # [kWt/kWe]
fuel_burn_slope = (fuel_burn_full_load - fuel_burn_half_load) / (1.0 - 0.5) # [kWt/kWe]
fuel_burn_intercept = fuel_burn_full_load - fuel_burn_slope * 1.0 # [kWt/kWe_rated]
fuel_burn_slope, fuel_burn_intercept = fuel_slope_and_intercept(;
electric_efficiency_full_load = p.s.chp.electric_efficiency_full_load,
electric_efficiency_half_load = p.s.chp.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_unit=1
)

# Conditionally add dvFuelBurnYIntercept if coefficient p.FuelBurnYIntRate is greater than ~zero
if abs(fuel_burn_intercept) > 1.0E-7
Expand Down Expand Up @@ -286,10 +287,12 @@ function add_MG_storage_dispatch_constraints(m,p)
)
)

# Minimum state of charge
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
m[:dvMGStoredEnergy][s, tz, ts] >= p.s.storage.attr["ElectricStorage"].soc_min_fraction * m[:dvStorageEnergy]["ElectricStorage"]
)
if p.s.storage.attr["ElectricStorage"].soc_min_applies_during_outages
# Minimum state of charge
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
m[:dvMGStoredEnergy][s, tz, ts] >= p.s.storage.attr["ElectricStorage"].soc_min_fraction * m[:dvStorageEnergy]["ElectricStorage"]
)
end

# Dispatch to MG electrical storage is no greater than inverter capacity
# and can't charge the battery unless binMGStorageUsed = 1
Expand Down
4 changes: 2 additions & 2 deletions src/core/bau_inputs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,10 @@ function bau_outage_check(critical_loads_kw::AbstractArray, pv_kw_series::Abstra
if length(pv_kw_series) == 0
pv_kw_series = zeros(length(critical_loads_kw))
end
fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr = generator_fuel_slope_and_intercept(
fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr = fuel_slope_and_intercept(
electric_efficiency_full_load=gen.electric_efficiency_full_load,
electric_efficiency_half_load=gen.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_gal=gen.fuel_higher_heating_value_kwh_per_gal
fuel_higher_heating_value_kwh_per_unit=gen.fuel_higher_heating_value_kwh_per_gal
)
for (i, (load, pv)) in enumerate(zip(critical_loads_kw, pv_kw_series))
unmet = load - pv
Expand Down
4 changes: 2 additions & 2 deletions src/core/chp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function CHP(d::Dict;
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`"))
elseif typeof(chp.installed_cost_per_kw) == Vector && length(chp.installed_cost_per_kw) == 1
elseif typeof(chp.installed_cost_per_kw) <: Array && length(chp.installed_cost_per_kw) == 1
chp.installed_cost_per_kw = chp.installed_cost_per_kw[1]
elseif isempty(chp.tech_sizes_for_cost_curve) && isempty(chp.installed_cost_per_kw)
update_installed_cost_params = true
Expand Down Expand Up @@ -541,4 +541,4 @@ function get_size_class_from_size(chp_elec_size_heuristic_kw, class_bounds, n_cl
end
end
return size_class
end
end
8 changes: 5 additions & 3 deletions src/core/doe_commercial_reference_building_loads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,13 @@ function blend_and_scale_doe_profiles(
if length(monthly_energies) == 12
monthly_scaler = length(blended_doe_reference_names)
end
for idx in 1:length(profiles)
profiles[idx] .*= total_kwh / annual_kwhs[idx] / monthly_scaler
for idx in eachindex(profiles)
if !(annual_kwhs[idx] == 0.0)
profiles[idx] .*= total_kwh / annual_kwhs[idx] / monthly_scaler
end
end
end
for idx in 1:length(profiles) # scale the profiles
for idx in eachindex(profiles) # scale the profiles
profiles[idx] .*= blended_doe_reference_percents[idx]
end
sum(profiles)
Expand Down
2 changes: 1 addition & 1 deletion src/core/electric_tariff.jl
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ function ElectricTariff(;

coincpeak_periods = Int64[]
if !isempty(coincident_peak_load_charge_per_kw)
coincpeak_periods = collect(1:length(coincident_peak_load_charge_per_kw))
coincpeak_periods = collect(eachindex(coincident_peak_load_charge_per_kw))
end

ElectricTariff(
Expand Down
4 changes: 4 additions & 0 deletions src/core/energy_storage/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ end
inverter_efficiency_fraction::Float64 = 0.96
rectifier_efficiency_fraction::Float64 = 0.96
soc_min_fraction::Float64 = 0.2
soc_min_applies_during_outages::Bool = false
soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5
can_grid_charge::Bool = off_grid_flag ? false : true
installed_cost_per_kw::Real = 910.0
Expand Down Expand Up @@ -176,6 +177,7 @@ Base.@kwdef struct ElectricStorageDefaults
inverter_efficiency_fraction::Float64 = 0.96
rectifier_efficiency_fraction::Float64 = 0.96
soc_min_fraction::Float64 = 0.2
soc_min_applies_during_outages::Bool = false
soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5
can_grid_charge::Bool = off_grid_flag ? false : true
installed_cost_per_kw::Real = 910.0
Expand Down Expand Up @@ -214,6 +216,7 @@ struct ElectricStorage <: AbstractElectricStorage
inverter_efficiency_fraction::Float64
rectifier_efficiency_fraction::Float64
soc_min_fraction::Float64
soc_min_applies_during_outages::Bool
soc_init_fraction::Float64
can_grid_charge::Bool
installed_cost_per_kw::Real
Expand Down Expand Up @@ -301,6 +304,7 @@ struct ElectricStorage <: AbstractElectricStorage
s.inverter_efficiency_fraction,
s.rectifier_efficiency_fraction,
s.soc_min_fraction,
s.soc_min_applies_during_outages,
s.soc_init_fraction,
s.can_grid_charge,
s.installed_cost_per_kw,
Expand Down
6 changes: 3 additions & 3 deletions src/core/urdb.jl
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM

# NOTE: periods are zero indexed
if dayofweek(Date(year, month, day)) < 6 # Monday == 1
period = d["energyweekdayschedule"][month][hour] + 1
period = Int(d["energyweekdayschedule"][month][hour] + 1)
else
period = d["energyweekendschedule"][month][hour] + 1
period = Int(d["energyweekendschedule"][month][hour] + 1)
end
# workaround for cases where there are different numbers of tiers in periods
n_tiers_in_period = length(d["energyratestructure"][period])
Expand Down Expand Up @@ -302,7 +302,7 @@ Convert an unexpected type::Matrix from URDB into an Array
- Observed while using REopt.jl with PyJulia/PyCall
"""
function convert_matrix_to_array(M::AbstractMatrix)
return [M[r,:] for r in 1:size(M,1)]
return [M[r,:] for r in axes(M, 1)]
end

"""
Expand Down
20 changes: 10 additions & 10 deletions src/core/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function dictkeys_tosymbols(d::Dict)
end
end
if k in [
"fuel_cost_per_mmbtu", "wholesale_rate",
"fuel_cost_per_mmbtu", "wholesale_rate", "export_rate_beyond_net_metering_limit",
# for ERP
"generator_size_kw", "generator_operational_availability",
"generator_failure_to_start", "generator_mean_time_to_failure",
Expand Down Expand Up @@ -480,29 +480,29 @@ function get_monthly_time_steps(year::Int; time_steps_per_hour=1)
end

"""
generator_fuel_slope_and_intercept(;
fuel_slope_and_intercept(;
electric_efficiency_full_load::Real, [kWhe/kWht]
electric_efficiency_half_load::Real, [kWhe/kWht]
fuel_higher_heating_value_kwh_per_gal::Real
fuel_higher_heating_value_kwh_per_unit::Real
)
return Tuple{<:Real,<:Real} where
first value is diesel fuel burn slope [gal/kWhe]
secnod value is diesel fuel burn intercept [gal/hr]
first value is fuel burn slope [<fuel unit>/kWhe]
second value is fuel burn intercept [<fuel unit>/hr]
"""
function generator_fuel_slope_and_intercept(;
function fuel_slope_and_intercept(;
electric_efficiency_full_load::Real,
electric_efficiency_half_load::Real,
fuel_higher_heating_value_kwh_per_gal::Real
fuel_higher_heating_value_kwh_per_unit::Real
)
fuel_burn_full_load_kwht = 1.0 / electric_efficiency_full_load # [kWe_rated/(kWhe/kWht)]
fuel_burn_half_load_kwht = 0.5 / electric_efficiency_half_load # [kWe_rated/(kWhe/kWht)]
fuel_slope_kwht_per_kwhe = (fuel_burn_full_load_kwht - fuel_burn_half_load_kwht) / (1.0 - 0.5) # [kWht/kWhe]
fuel_intercept_kwht_per_hr = fuel_burn_full_load_kwht - fuel_slope_kwht_per_kwhe * 1.0 # [kWht/hr]
fuel_slope_gal_per_kwhe = fuel_slope_kwht_per_kwhe / fuel_higher_heating_value_kwh_per_gal # [gal/kWhe]
fuel_intercept_gal_per_hr = fuel_intercept_kwht_per_hr / fuel_higher_heating_value_kwh_per_gal # [gal/hr]
fuel_slope_unit_per_kwhe = fuel_slope_kwht_per_kwhe / fuel_higher_heating_value_kwh_per_unit # [<fuel unit>/kWhe]
fuel_intercept_unit_per_hr = fuel_intercept_kwht_per_hr / fuel_higher_heating_value_kwh_per_unit # [<fuel unit>/hr]

return fuel_slope_gal_per_kwhe, fuel_intercept_gal_per_hr
return fuel_slope_unit_per_kwhe, fuel_intercept_unit_per_hr
end

function convert_temp_degF_to_Kelvin(degF::Float64)
Expand Down
2 changes: 1 addition & 1 deletion src/mpc/structs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ function MPCElectricTariff(d::Dict)
energy_rates = d["energy_rates"]

monthly_demand_rates = get(d, "monthly_demand_rates", [0.0])
time_steps_monthly = get(d, "time_steps_monthly", [collect(1:length(energy_rates))])
time_steps_monthly = get(d, "time_steps_monthly", [collect(eachindex(energy_rates))])
monthly_previous_peak_demands = get(d, "monthly_previous_peak_demands", [0.0])

tou_demand_rates = get(d, "tou_demand_rates", Float64[])
Expand Down
10 changes: 5 additions & 5 deletions src/outagesim/backup_reliability.jl
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ gen_battery_prob_matrix
function shift_gen_battery_prob_matrix!(gen_battery_prob_matrix::Matrix, shift_vector::Vector{Int})
M = size(gen_battery_prob_matrix, 1)

for i in 1:length(shift_vector)
for i in eachindex(shift_vector)
s = shift_vector[i]
if s < 0
#TODO figure out why implementation of cirshift! is working locally but not on server
Expand Down Expand Up @@ -787,20 +787,20 @@ function backup_reliability_reopt_inputs(;d::Dict, p::REoptInputs, r::Dict = Dic
end
r2[:generator_size_kw] = replace!([diesel_kw + prime_kw] ./ r2[:num_generators], Inf => 0) # at least one gen kw will be 0 because of error thrown above
if diesel_kw > 0
fuel_slope, fuel_intercept = generator_fuel_slope_and_intercept(
fuel_slope, fuel_intercept = fuel_slope_and_intercept(
electric_efficiency_full_load=p.s.generator.electric_efficiency_full_load,
electric_efficiency_half_load=p.s.generator.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_gal=p.s.generator.fuel_higher_heating_value_kwh_per_gal
fuel_higher_heating_value_kwh_per_unit=p.s.generator.fuel_higher_heating_value_kwh_per_gal
)
r2[:generator_fuel_burn_rate_per_kwh] = [fuel_slope]
r2[:generator_fuel_intercept_per_hr] = [fuel_intercept]
r2[:fuel_limit] = [p.s.generator.fuel_avail_gal]
end
if prime_kw > 0
fuel_slope, fuel_intercept = generator_fuel_slope_and_intercept(
fuel_slope, fuel_intercept = fuel_slope_and_intercept(
electric_efficiency_full_load=p.s.chp.electric_efficiency_full_load,
electric_efficiency_half_load=p.s.chp.electric_efficiency_half_load,
fuel_higher_heating_value_kwh_per_gal=p.s.chp.fuel_higher_heating_value_kwh_per_gal
fuel_higher_heating_value_kwh_per_unit=1
)
r2[:generator_fuel_burn_rate_per_kwh] = [fuel_slope]
r2[:generator_fuel_intercept_per_hr] = [fuel_intercept]
Expand Down
Loading

2 comments on commit d7b888a

@hdunham
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error while trying to register: Version 0.44.0 already exists

Please sign in to comment.