diff --git a/CHANGELOG.md b/CHANGELOG.md index 7126b72c0..04a384e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,13 +23,45 @@ Classify the change according to the following categories: ### Deprecated ### Removed -## dev +## Develop - 2023-02-02 +### Added +- Constraint on wind sizing based on Site.land_acres +- New Wind input **acres_per_kw**, defaults to 0.03 + +### Changed +- Changed default **year** in ElectricLoad to be 2017 if using a CRB model and 2022 otherwise. +- Removed default year in URDBrate() functions, since year is always supplied to this function. +## Develop +### Fixed +- Fixed calculation of ["Financial"]["lifecycle_om_costs_before_tax_bau"] (was previously showing after tax result) +- Added **bau_annual_emissions_tonnes_SO2** to the bau_outputs dict in results.jl and removed duplicate **bau_annual_emissions_tonnes_NOx** result +### Added +- Descriptions/help text for many inputs and outputs + +## Develop - 2023-02-01 +## v0.25.0 ### Added - multi-node MPC modeling capability - more MPC outputs (e.g. Costs, ElectricStorage.to_load_series_kw) +- throw error if outage_durations and outage_probabilities not the same length +- throw error if length of outage_probabilities is >= 1 and sum of outage_probabilities is not equal to 1 +- small incentive to minimize unserved load in each outage, not just the max over outage start times (makes expected outage results more realist and fixes same inputs giving different results) +- add `Outages` output **generator_fuel_used_per_outage** which is the sum over backup generators +### Changed +- remove _series from non-timeseries outage output names +- make the use of _ in multiple outages output names consistent +- updates multiple outage test values that changed due to fixing timestep bug +- Updated the following default values: + - PV, Wind, Storage, CHP, GHP, Hot Water Storage, Cold Water Storage, Electric Storage: **federal_itc_fraction(PV,Wind, CHP,GHP)** and **total_itc_fraction(Hot Water Storage, Cold Water Storage, Electric Storage)** to 0.3 (30%) + - PV, Wind, Storage, CHP, GHP, Hot Water Storage, Cold Water Storage, Electric Storage: **macrs_bonus_fraction** to 0.8 (80%) + - Hot Water Storage and Cold Water Storage: **macrs_itc_reduction** to 0.5 (50%) + - Hot Water Storage and Cold Water Storage: **macrs_option_years** to 7 years ### Fixed - PV results for all multi-node scenarios - MPC objective definition w/o ElectricStorage +- fixed mulitple outages timestep off-by-one bug +### Removed +- Wind ITC no longer determined based on size class. Removed all size class dependencies from wind.jl ## v0.24.0 ### Changed @@ -151,14 +183,18 @@ The following name changes were made: - Bug fix to report accurate wind ["year_one_to_load_series_kw"] in results/wind.jl (was previously not accounting for curtailed wind) ## v0.16.2 +### Changed - Update PV defaults to tilt=10 for rooftop, tilt = abs(lat) for ground mount, azimuth = 180 for northern lats, azimuth = 0 for southern lats. +### Fixed - bug fix for Generator inputs to allow for time_steps_per_hour > 1 - change various `Float64` types to `Real` to allow integers too ## v0.16.1 +### Fixed - bug fix for outage simulator when `microgrid_only=true` ## v0.16.0 +### Added Allows users to model "off-grid" systems as a year-long outage: - add flag to "turn on" off-grid modeling `Settings.off_grid_flag` - when `off_grid_flag` is "true", adjust default values in core/ `electric_storage`, `electric_load`, `financial`, `generator`, `pv` @@ -166,93 +202,118 @@ Allows users to model "off-grid" systems as a year-long outage: - add minimum load met percent input and constraint - add generator replacement year and cost (for off-grid and on-grid) - add off-grid additional annual costs (tax deductible) and upfront capital costs (depreciable via straight line depreciation) - +### Changed Name changes: - consistently append `_before_tax` and `_after_tax` to results names - change all instances of `timestep` to `time_step` and `timesteps` to `time_steps` - Other changes: - report previously missing lcc breakdown components, all reported in `results/financial.jl` - change variable types from Float to Real to allow users to enter Ints (where applicable) - `year_one_coincident_peak_cost_after_tax` is now correctly multiplied by `(1 - p.s.financial.offtaker_tax_pct)` ## v0.15.2 +### Fixed - bug fix for 15 & 30 minute electric, heating, and cooling loads - bug fix for URDB fixed charges - bug fix for default `Wind` `installed_cost_per_kw` and `federal_itc_pct` ## v0.15.1 +### Added - add `AbsorptionChiller` technology - add `ElectricStorage.minimum_avg_soc_fraction` input and constraint ## v0.15.0 +### Fixed - bug fix in outage_simulator +### Changed - allow Real Generator inputs (not just Float64) - add "_series" to "Outages" outputs that are arrays [breaking] ## v0.14.0 +### Changed - update default values from v2 of API [breaking] +### Added - add ElectricStorage degradation accounting and maintenance strategies - finish cooling loads ## v0.13.0 -- fix bugs for time_steps_per_hour != 1 +### Added - add FlexibleHVAC model (still testing) - start thermal energy storage modeling -- refactor `Storage` as `ElectricStorage` - add `ExistingBoiler` and `ExistingChiller` - add `MPCLimits` inputs: - `grid_draw_limit_kw_by_time_step` - `export_limit_kw_by_time_step` +### Changed +- refactor `Storage` as `ElectricStorage` +### Fixed +- fix bugs for time_steps_per_hour != 1 + ## v0.12.4 +### Removed - rm "Lite" from docs +### Changed - prioritize `urdb_response` over `urdb_label` in `ElectricTariff` ## v0.12.3 +### Added - add utils for PVwatts: `get_ambient_temperature` and `get_pvwatts_prodfactor` ## v0.12.2 +### Added - add CHP technology, including supplementary firing - add URDB "sell" value from `energyratestructure` to wholesale rate - update docs +### Changed - allow annual or monthly energy rate w/o demand rate - allow integer latitude/longitude ## v0.12.1 +### Added - add ExistingBoiler and CRB heating loads ## v0.12.0 +### Changed - change all output keys starting with "total_" or "net_" to "lifecycle_" (except "net_present_cost") -- bug fix in urdb.jl when rate_name not found - update pv results for single PV in an array +### Fixed +- bug fix in urdb.jl when rate_name not found ## v0.11.0 +### Added - add ElectricLoad.blended_doe_reference_names & blended_doe_reference_percents - add ElectricLoad.monthly_totals_kwh builtin profile scaling - add ElectricTariff inputs: `add_monthly_rates_to_urdb_rate`, `tou_energy_rates_per_kwh`, `add_tou_energy_rates_to_urdb_rate`, `coincident_peak_load_charge_per_kw`, `coincident_peak_load_active_time_steps` +### Fixed - handle multiple PV outputs ## v0.10.0 +### Added - add modeling capability for tiered rates (energy, TOU demand, and monthly demand charges) - all of these tiered rates require binaries, which are conditionally added to the model - add modeling capability for lookback demand charges -- removed "_us_dollars" from all names and generally aligned names with API - add more outputs from the API (eg. `initial_capital_costs`) - add option to run Business As Usual scenario in parallel with optimal scenario (default is `true`) - add incentives (and cost curves) to `Wind` and `Generator` -- fixed bug in URDB fixed charges +### Changed +- removed "_us_dollars" from all names and generally aligned names with API - renamed `outage_start(end)_time_step` to `outage_start(end)_time_step` +### Fixed +- fixed bug in URDB fixed charges ## v0.9.0 +### Changed - `ElectricTariff.NEM` boolean is now determined by `ElectricUtility.net_metering_limit_kw` (true if limit > 0) +### Added - add `ElectricUtility` inputs for `net_metering_limit_kw` and `interconnection_limit_kw` - add binary choice for net metering vs. wholesale export - add `ElectricTariff.export_rate_beyond_net_metering_limit` input (scalar or vector allowed) - add `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` tech inputs (`PV`, `Wind`, `Generator`) ## v0.8.0 +### Added - add `Wind` module, relying on System Advisor Model Wind module for production factors and Wind Toolkit for resource data - new `ElectricTariff` input options: - `urdb_utility_name` and `urdb_rate_name` @@ -261,73 +322,76 @@ Other changes: - tax, production, and capacity incentives for PV (compatible with any energy generation technology) - technology cost curve modeling capability - both of these capabilities are only used for the technologies that require them (based on input values), unlike the API which always models these capabilities (and therefore always includes the binary variables). +- Three new tests: Wind, Blended Tariff and Complex Incentives (which aligns with API results) +### Changed - `cost_per_kw[h]` input fields are now `installed_cost_per_kw[h]` to distinguish it from other costs like `om_cost_per_kw[h]` - Financial input field refactored: `two_party_ownership` -> `third_party_ownership` - `total_itc_pct` -> `federal_itc_pct` on technology inputs -- Three new tests: Wind, Blended Tariff and Complex Incentives (which aligns with API results) ## v0.7.3 -##### bug fixes +### Fixed - outage results processing would fail sometimes when an integer variable was not exact (e.g. 1.000000001) - fixed `simulate_outages` for revised results formats (key names changed to align with the REopt API) ## v0.7.2 -#### Improvements +### Added - add PV.production_factor_series input (can skip PVWatts call) - add `run_mpc` capability, which dispatches DER for minimum energy cost over an arbitrary time horizon ## v0.7.1 -##### bug fixes +### Fixed - ElectricLoad.city default is empty string, must be filled in before annual_kwh look up ## v0.7.0 -#### Improvements +### Removed - removed Storage.can_grid_export +### Added - add optional integer constraint to prevent simultaneous export and import of power - add warnings when adding integer variables - add ability to add LinDistFlow constraints to multinode models +### Changed - no longer require `ElectricLoad.city` input (look up ASHRAE climate zone from lat/lon) - compatible with Julia 1.6 ## v0.6.0 -#### Improvements +### Added - add multi-node (site) capability for PV and Storage - started documentation process using Github Pages and Documenter.jl +### Changed - restructured outputs to align with the input structure, for example top-level keys added for `ElectricTariff` and `PV` in the outputs ## v0.5.3 -#### Improvements +### Changed - compatible with Julia 1.5 ## v0.5.2 -#### bug fixes +### Fixed - outage_simulator.jl had bug with summing over empty `Any[]` - -#### Improvements +### Added - add optional `microgrid_only` arg to simulate_outages ## v0.5.1 -#### Improvements +### Added - added outage dispatch outputs and speed up their derivation +### Removed - removed redundant generator minimum turn down constraint ## v0.5.0 -#### bug fixes +### Fixed - 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 - -#### Improvements +#### Added - add `unserved_load_per_outage` output ## v0.4.1 -#### bug fixes +### Fixed - removed `total_unserved_load` output because it can take hours to generate and can error out when outage indices are not consecutive -#### Improvements +### Added - add @info for time spent processing results ## v0.4.0 -#### Improvements +### Added - add `simulate_outages` function (similar to REopt API outage simulator) - removed MutableArithmetics package from Project.toml (since JuMP now has method for `value(::MutableArithmetics.Zero)`) - add outage related outputs: @@ -338,27 +402,28 @@ Other changes: - mg_storage_upgrade_cost - dvUnservedLoad array - max_outage_cost_per_outage_duration +### Changed - allow value_of_lost_load_per_kwh values to be subtype of Real (rather than only Real) - add `run_reopt` method for scenario Dict ## v0.3.0 -#### Improvements +### 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 -#### bug fixes +### Fixed - allow non-integer `outage_probabilities` - correct `total_unserved_load` output - don't `add_min_hours_crit_ld_met_constraint` unless `min_resil_time_steps <= length(elecutil.outage_time_steps)` ## v0.2.0 -#### Improvements +### Added - add support for custom ElectricLoad `loads_kw` input - include existing capacity in microgrid upgrade cost - previously only had to pay to upgrade new capacity - implement ElectricLoad `loads_kw_is_net` and `critical_loads_kw_is_net` - add existing PV production to raw load profile if `true` - add `min_resil_time_steps` input and optional constraint for minimum time_steps that critical load must be met in every outage -#### bug fixes +### Fixed - enforce storage cannot grid charge ## v0.1.1 Fix build.jl diff --git a/Project.toml b/Project.toml index 7ebfdf882..6c6bf9e69 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.24.0" +version = "0.25.0" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3" diff --git a/README.md b/README.md index 07475fe02..809e99639 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # REopt Julia package -This package is currently under development, but it now has **most** of capabilities of the REopt model used in the [REopt API](https://github.com/NREL/REopt_API). We expect to have the first stable release by Fall 2022. +This package is currently under development, but it now has all of the capabilities of the REopt model used in the [REopt API](https://github.com/NREL/REopt_API). We expect to have the first stable release by Summer 2023. For more information please see the documentation: diff --git a/docs/src/developer/inputs.md b/docs/src/developer/inputs.md index b10443a0d..9a380eafc 100644 --- a/docs/src/developer/inputs.md +++ b/docs/src/developer/inputs.md @@ -41,7 +41,7 @@ The set maps are best explained with an example. The `techs_by_exportbin` map us 1. `:NEM` (Net Energy Metering) 2. `:WHL` (Wholesale) 3. `:EXC` (Excess, beyond NEM)) -The bins that a technolgy can access are determined by the technologies attributes `can_net_meter`, `can_wholesale`, and `can_export_beyond_nem_limit`. So if `PV.can_net_meter = true`, `Wind.can_net_meter = true` and all the other attributes are `false` then the `techs_by_exportbin` will only have one non-empty key: +The bins that a technology can access are determined by the technologies attributes `can_net_meter`, `can_wholesale`, and `can_export_beyond_nem_limit`. So if `PV.can_net_meter = true`, `Wind.can_net_meter = true` and all the other attributes are `false` then the `techs_by_exportbin` will only have one non-empty key: ```julia techs_by_exportbin = Dict( :NEM => ["PV", "Wind"], diff --git a/src/constraints/outage_constraints.jl b/src/constraints/outage_constraints.jl index bfd40481f..102490c93 100644 --- a/src/constraints/outage_constraints.jl +++ b/src/constraints/outage_constraints.jl @@ -30,8 +30,8 @@ function add_dv_UnservedLoad_constraints(m,p) # effective load balance (with slack in dvUnservedLoad) @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[:dvUnservedLoad][s, tz, ts] >= p.s.electric_load.critical_loads_kw[tz+ts] - - sum( m[:dvMGRatedProduction][t, s, tz, ts] * p.production_factor[t, tz+ts] * p.levelization_factor[t] + m[:dvUnservedLoad][s, tz, ts] >= p.s.electric_load.critical_loads_kw[tz+ts-1] + - sum( m[:dvMGRatedProduction][t, s, tz, ts] * p.production_factor[t, tz+ts-1] * p.levelization_factor[t] - m[:dvMGProductionToStorage][t, s, tz, ts] - m[:dvMGCurtail][t, s, tz, ts] for t in p.techs.elec ) @@ -50,7 +50,7 @@ end function add_outage_cost_constraints(m,p) @constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps], - m[:dvMaxOutageCost][s] >= p.pwf_e * sum(p.value_of_lost_load_per_kwh[tz+ts] * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) + m[:dvMaxOutageCost][s] >= p.pwf_e * sum(p.value_of_lost_load_per_kwh[tz+ts-1] * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) ) @expression(m, ExpectedOutageCost, @@ -98,7 +98,7 @@ function add_MG_production_constraints(m,p) # Electrical production sent to storage or export must be less than technology's rated production @constraint(m, [t in p.techs.elec, 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[:dvMGProductionToStorage][t, s, tz, ts] + m[:dvMGCurtail][t, s, tz, ts] <= - p.production_factor[t, tz+ts] * p.levelization_factor[t] * m[:dvMGRatedProduction][t, s, tz, ts] + p.production_factor[t, tz+ts-1] * p.levelization_factor[t] * m[:dvMGRatedProduction][t, s, tz, ts] ) @constraint(m, [t in p.techs.elec, 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], @@ -115,7 +115,7 @@ function add_MG_fuel_burn_constraints(m,p) # 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], m[:dvMGFuelUsed][t, s, tz] == p.s.generator.fuel_slope_gal_per_kwh * p.hours_per_time_step * p.levelization_factor[t] * - sum( p.production_factor[t, tz+ts] * m[:dvMGRatedProduction][t, s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) + sum( p.production_factor[t, tz+ts-1] * m[:dvMGRatedProduction][t, s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) + p.s.generator.fuel_intercept_gal_per_hr * p.hours_per_time_step * sum( m[:binMGGenIsOnInTS][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) ) diff --git a/src/core/absorption_chiller.jl b/src/core/absorption_chiller.jl index 6dcf4e4bb..8a9adc4a0 100644 --- a/src/core/absorption_chiller.jl +++ b/src/core/absorption_chiller.jl @@ -35,15 +35,14 @@ chp_prime_mover::String = "" # Informs thermal_consumption_hot_water_or_steam if not provided # Defaults for fields below are dependent on thermal_consumption_hot_water_or_steam and max cooling load - installed_cost_per_ton::Union{Float64, Nothing} = nothing - om_cost_per_ton::Union{Float64, Nothing} = nothing - min_ton::Float64 = 0.0, - max_ton::Float64 = BIG_NUMBER, - cop_thermal::Union{Float64, Nothing} = nothing, - cop_electric::Float64 = 14.1, - om_cost_per_ton::Union{Float64, Nothing} = nothing, - macrs_option_years::Float64 = 0, - macrs_bonus_fraction::Float64 = 0 + installed_cost_per_ton::Union{Float64, Nothing} = nothing # Thermal power-based cost of absorption chiller (3.5 to 1 ton to kwt) + om_cost_per_ton::Union{Float64, Nothing} = nothing # Yearly fixed O&M cost on a thermal power (ton) basis + min_ton::Float64 = 0.0, # Minimum thermal power size constraint for optimization + max_ton::Float64 = BIG_NUMBER, # Maximum thermal power size constraint for optimization + cop_thermal::Union{Float64, Nothing} = nothing, # Absorption chiller system coefficient of performance - conversion of hot thermal power input to usable cooling thermal energy output + cop_electric::Float64 = 14.1, # Absorption chiller electric consumption CoP from cooling tower heat rejection - conversion of electric power input to usable cooling thermal energy outpu + macrs_option_years::Float64 = 0, # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Float64 = 0 # Percent of upfront project costs to depreciate under MACRS ``` !!! Note diff --git a/src/core/boiler.jl b/src/core/boiler.jl index 03607eafb..e5c01f773 100644 --- a/src/core/boiler.jl +++ b/src/core/boiler.jl @@ -53,17 +53,17 @@ in addition to using the `ExistingBoiler` to meet the heating load. ```julia function Boiler(; - min_mmbtu_per_hour::Real = 0.0, - max_mmbtu_per_hour::Real = 0.0, - efficiency::Real = 0.8, + min_mmbtu_per_hour::Real = 0.0, # Minimum thermal power size + max_mmbtu_per_hour::Real = 0.0, # Maximum thermal power size + efficiency::Real = 0.8, # boiler system efficiency - conversion of fuel to usable heating thermal energy fuel_cost_per_mmbtu::Union{<:Real, AbstractVector{<:Real}} = 0.0, - macrs_option_years::Int = 0, - macrs_bonus_fraction::Real = 0.0, - installed_cost_per_mmbtu_per_hour::Real = 293000.0, - om_cost_per_mmbtu_per_hour::Real = 2930.0, - om_cost_per_mmbtu::Real = 0.0, + macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS + installed_cost_per_mmbtu_per_hour::Real = 293000.0, # Thermal power-based cost + om_cost_per_mmbtu_per_hour::Real = 2930.0, # Thermal power-based fixed O&M cost + om_cost_per_mmbtu::Real = 0.0, # Thermal energy-based variable O&M cost fuel_type::String = "natural_gas", # "restrict_to": ["natural_gas", "landfill_bio_gas", "propane", "diesel_oil", "uranium"] - can_supply_steam_turbine::Bool = true + can_supply_steam_turbine::Bool = true # If the boiler can supply steam to the steam turbine for electric production ) ``` """ diff --git a/src/core/chp.jl b/src/core/chp.jl index d23a20db8..fc479cde5 100644 --- a/src/core/chp.jl +++ b/src/core/chp.jl @@ -33,40 +33,40 @@ prime_movers = ["recip_engine", "micro_turbine", "combustion_turbine", "fuel_cel """ `CHP` is an optional REopt input with the following keys and default values: ```julia - prime_mover::Union{String, Nothing} = nothing Suggested to inform applicable default cost and performance - fuel_cost_per_mmbtu::Union{<:Real, AbstractVector{<:Real}} = [] REQUIRED + prime_mover::Union{String, Nothing} = nothing # Suggested to inform applicable default cost and performance. "restrict_to": ["recip_engine", "micro_turbine", "combustion_turbine", "fuel_cell"] + 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 # Required "custom inputs" if not providing prime_mover: - installed_cost_per_kw::Union{Float64, AbstractVector{Float64}} = NaN - tech_sizes_for_cost_curve::Union{Float64, AbstractVector{Float64}} = NaN - om_cost_per_kwh::Float64 = NaN - electric_efficiency_full_load::Float64 = NaN - electric_efficiency_half_load::Float64 = NaN - min_turn_down_fraction::Float64 = NaN - thermal_efficiency_full_load::Float64 = NaN - thermal_efficiency_half_load::Float64 = NaN - min_allowable_kw::Float64 = NaN - max_kw::Float64 = NaN + installed_cost_per_kw::Union{Float64, AbstractVector{Float64}} = NaN # Installed CHP system cost in \$/kW (based on rated electric power) + tech_sizes_for_cost_curve::Union{Float64, AbstractVector{Float64}} = NaN # Size of CHP systems corresponding to installed cost input points" + om_cost_per_kwh::Float64 = NaN # CHP non-fuel variable operations and maintenance costs in \$/kwh + electric_efficiency_full_load::Float64 = NaN # Electric efficiency of CHP prime-mover at full-load, HHV-basis + electric_efficiency_half_load::Float64 = NaN # Electric efficiency of CHP prime-mover at half-load, HHV-basis + min_turn_down_fraction::Float64 = NaN # Minimum CHP electric loading in fraction of capacity (size_kw) + thermal_efficiency_full_load::Float64 = NaN # CHP fraction of fuel energy converted to hot-thermal energy at full electric load + thermal_efficiency_half_load::Float64 = NaN # CHP fraction of fuel energy converted to hot-thermal energy at half electric load + min_allowable_kw::Float64 = NaN # Minimum CHP size (based on electric) that still allows the model to choose zero (e.g. no CHP system) + max_kw::Float64 = NaN # Maximum CHP size (based on electric) constraint for optimization. cooling_thermal_factor::Float64 = NaN # only needed with cooling load - unavailability_periods::AbstractVector{Dict} = Dict[] + unavailability_periods::AbstractVector{Dict} = Dict[] # CHP unavailability periods for scheduled and unscheduled maintenance, list of dictionaries with keys of "['month', 'start_week_of_month', 'start_day_of_week', 'start_hour', 'duration_hours'] all values are one-indexed and start_day_of_week uses 1 for Monday, 7 for Sunday # Optional inputs: - size_class::Union{Int, Nothing} = nothing - min_kw::Float64 = 0.0 + size_class::Union{Int, Nothing} = nothing # CHP size class for using appropriate default inputs + min_kw::Float64 = 0.0 # Minimum CHP size (based on electric) constraint for optimization fuel_type::String = "natural_gas" # "restrict_to": ["natural_gas", "landfill_bio_gas", "propane", "diesel_oil"] - om_cost_per_kw::Float64 = 0.0 - om_cost_per_hr_per_kw_rated::Float64 = 0.0 - supplementary_firing_capital_cost_per_kw::Float64 = 150.0 - supplementary_firing_max_steam_ratio::Float64 = 1.0 - supplementary_firing_efficiency::Float64 = 0.92 - standby_rate_per_kw_per_month::Float64 = 0.0 - reduces_demand_charges::Bool = true - can_supply_steam_turbine::Bool=false + om_cost_per_kw::Float64 = 0.0 # Annual CHP fixed operations and maintenance costs in \$/kw-yr + om_cost_per_hr_per_kw_rated::Float64 = 0.0 # CHP non-fuel variable operations and maintenance costs in \$/hr/kw_rated + supplementary_firing_capital_cost_per_kw::Float64 = 150.0 # Installed CHP supplementary firing system cost in \$/kW (based on rated electric power) + supplementary_firing_max_steam_ratio::Float64 = 1.0 # Ratio of max fired steam to un-fired steam production. Relevant only for combustion_turbine prime_mover + supplementary_firing_efficiency::Float64 = 0.92 # Thermal efficiency of the incremental steam production from supplementary firing. Relevant only for combustion_turbine prime_mover + standby_rate_per_kw_per_month::Float64 = 0.0 # Standby rate charged to CHP based on CHP electric power size + reduces_demand_charges::Bool = true # Boolean indicator if CHP does not reduce demand charges + can_supply_steam_turbine::Bool=false # If CHP can supply steam to the steam turbine for electric production macrs_option_years::Int = 5 - macrs_bonus_fraction::Float64 = 1.0 + macrs_bonus_fraction::Float64 = 0.8 macrs_itc_reduction::Float64 = 0.5 - federal_itc_fraction::Float64 = 0.1 + federal_itc_fraction::Float64 = 0.3 federal_rebate_per_kw::Float64 = 0.0 state_ibi_fraction::Float64 = 0.0 state_ibi_max::Float64 = 1.0e10 @@ -132,9 +132,9 @@ Base.@kwdef mutable struct CHP <: AbstractCHP can_supply_steam_turbine::Bool = false macrs_option_years::Int = 5 - macrs_bonus_fraction::Float64 = 1.0 + macrs_bonus_fraction::Float64 = 0.8 macrs_itc_reduction::Float64 = 0.5 - federal_itc_fraction::Float64 = 0.1 + federal_itc_fraction::Float64 = 0.3 federal_rebate_per_kw::Float64 = 0.0 state_ibi_fraction::Float64 = 0.0 state_ibi_max::Float64 = 1.0e10 diff --git a/src/core/electric_load.jl b/src/core/electric_load.jl index 3fdab5f9e..f7602d1d6 100644 --- a/src/core/electric_load.jl +++ b/src/core/electric_load.jl @@ -32,10 +32,10 @@ ```julia loads_kw::Array{<:Real,1} = Real[], path_to_csv::String = "", # for csv containing loads_kw - year::Int = 2020, # used in ElectricTariff to align rate schedule with weekdays/weekends doe_reference_name::String = "", blended_doe_reference_names::Array{String, 1} = String[], blended_doe_reference_percents::Array{<:Real,1} = Real[], + year::Int = doe_reference_name ≠ nothing && blended_doe_reference_names ≠ nothing ? 2017 : 2022, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles must use 2017. If providing load data, specify year of data. city::String = "", annual_kwh::Union{Real, Nothing} = nothing, monthly_totals_kwh::Array{<:Real,1} = Real[], @@ -94,6 +94,11 @@ Each `city` and `doe_reference_name` combination has a default `annual_kwh`, or you can provide your own `annual_kwh` or `monthly_totals_kwh` and the reference profile will be scaled appropriately. + + +!!! note "Year" + The ElectricLoad `year` is used in ElectricTariff to align rate schedules with weekdays/weekends. If providing your own `loads_kw`, ensure the `year` matches the year of your data. + If utilizing `doe_reference_name` or `blended_doe_reference_names`, the default year of 2017 is used because these load profiles start on a Sunday. """ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off of (critical_)loads_kw_is_net loads_kw::Array{Real,1} @@ -109,10 +114,10 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o off_grid_flag::Bool = false, loads_kw::Array{<:Real,1} = Real[], path_to_csv::String = "", - year::Int = 2020, # used in ElectricTariff to align rate schedule with weekdays/weekends doe_reference_name::String = "", blended_doe_reference_names::Array{String, 1} = String[], blended_doe_reference_percents::Array{<:Real,1} = Real[], + year::Int = doe_reference_name ≠ "" && blended_doe_reference_names ≠ String[] ? 2017 : 2022, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles must use 2017. If providing load data, specify year of data. city::String = "", annual_kwh::Union{Real, Nothing} = nothing, monthly_totals_kwh::Array{<:Real,1} = Real[], @@ -172,6 +177,7 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o loads_kw = blend_and_scale_doe_profiles(BuiltInElectricLoad, latitude, longitude, year, blended_doe_reference_names, blended_doe_reference_percents, city, annual_kwh, monthly_totals_kwh) + # TODO: Should also warn here about year 2017 else throw(@error("Cannot construct ElectricLoad. You must provide either [loads_kw], [doe_reference_name, city], [doe_reference_name, latitude, longitude], diff --git a/src/core/electric_tariff.jl b/src/core/electric_tariff.jl index bd9b751d8..c8cfdf91c 100644 --- a/src/core/electric_tariff.jl +++ b/src/core/electric_tariff.jl @@ -73,23 +73,21 @@ end urdb_response::Dict=Dict(), urdb_utility_name::String="", urdb_rate_name::String="", - year::Int=2020, - NEM::Bool=false, - wholesale_rate::T1=nothing, - export_rate_beyond_net_metering_limit::T2=nothing, - monthly_energy_rates::Array=[], - monthly_demand_rates::Array=[], - blended_annual_energy_rate::S=nothing, - blended_annual_demand_rate::R=nothing, - add_monthly_rates_to_urdb_rate::Bool=false, - tou_energy_rates_per_kwh::Array=[], - add_tou_energy_rates_to_urdb_rate::Bool=false, + wholesale_rate::T1=nothing, # Price of electricity sold back to the grid in absence of net metering. Can be a scalar value, which applies for all-time, or an array with time-sensitive values. If an array is input then it must have a length of 8760, 17520, or 35040. The inputed array values are up/down-sampled using mean values to match the Settings.time_steps_per_hour. + export_rate_beyond_net_metering_limit::T2=nothing, # Price of electricity sold back to the grid beyond total annual grid purchases, regardless of net metering. Can be a scalar value, which applies for all-time, or an array with time-sensitive values. If an array is input then it must have a length of 8760, 17520, or 35040. The inputed array values are up/down-sampled using mean values to match the Settings.time_steps_per_hour + monthly_energy_rates::Array=[], # Array (length of 12) of blended energy rates in dollars per kWh + monthly_demand_rates::Array=[], # Array (length of 12) of blended demand charges in dollars per kW + blended_annual_energy_rate::S=nothing, # Annual blended energy rate [\$ per kWh] (total annual energy in kWh divided by annual cost in dollars) + blended_annual_demand_rate::R=nothing, # Average monthly demand charge [\$ per kW per month]. Rate will be applied to monthly peak demand. + add_monthly_rates_to_urdb_rate::Bool=false, # Set to 'true' to add the monthly blended energy rates and demand charges to the URDB rate schedule. Otherwise, blended rates will only be considered if a URDB rate is not provided. + tou_energy_rates_per_kwh::Array=[], # Time-of-use energy rates, provided by user. Must be an array with length equal to number of timesteps per year. + add_tou_energy_rates_to_urdb_rate::Bool=false, # Set to 'true' to add the tou energy rates to the URDB rate schedule. Otherwise, tou energy rates will only be considered if a URDB rate is not provided. remove_tiers::Bool=false, 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[] + coincident_peak_load_active_time_steps::Vector{Vector{Int64}}=[Int64[]], # The optional coincident_peak_load_charge_per_kw will apply at the max grid-purchased power during these timesteps. Note timesteps are indexed to a base of 1 not 0. + coincident_peak_load_charge_per_kw::AbstractVector{<:Real}=Real[] # Optional coincident peak demand charge that is applied to the max load during the timesteps specified in coincident_peak_load_active_time_steps. ) where { T1 <: Union{Nothing, Real, Array{<:Real}}, T2 <: Union{Nothing, Real, Array{<:Real}}, @@ -97,6 +95,18 @@ end R <: Union{Nothing, Real} } ``` +!!! note "Export Rates" + There are three Export tiers and their associated export rates (negative cost values): + 1. NEM (Net Energy Metering) - set to the energy rate (or tier with the lowest energy rate, if tiered) + 2. WHL (Wholesale) - set to wholesale_rate + 3. EXC (Excess, beyond NEM) - set to export_rate_beyond_net_metering_limit + + Only one of NEM and Wholesale can be exported into due to the binary constraints. + Excess can be exported into in the same time step as NEM. + + Excess is meant to be combined with NEM: NEM export is limited to the total grid purchased energy in a year and some + utilities offer a compensation mechanism for export beyond the site load. + The Excess tier is not available with the Wholesale tier. !!! note "NEM input" The `NEM` boolean is determined by the `ElectricUtility.net_metering_limit_kw`. There is no need to pass in a `NEM` @@ -111,7 +121,7 @@ function ElectricTariff(; urdb_response::Dict=Dict(), urdb_utility_name::String="", urdb_rate_name::String="", - year::Int=2020, + year::Int=2022, # Will be passed from ElectricLoad time_steps_per_hour::Int=1, NEM::Bool=false, wholesale_rate::T1=nothing, @@ -318,23 +328,23 @@ function ElectricTariff(; end exc_rate = create_export_rate(export_rate_beyond_net_metering_limit, length(energy_rates), time_steps_per_hour) - if !NEM & (sum(whl_rate) >= 0) + if !NEM & (sum(whl_rate) >= 0) # no NEM or WHL export_rates = Dict{Symbol, AbstractArray}() export_bins = Symbol[] - elseif !NEM + elseif !NEM # no NEM, with WHL export_bins = [:WHL] export_rates = Dict(:WHL => whl_rate) - elseif (sum(whl_rate) >= 0) + elseif (sum(whl_rate) >= 0) # NEM, no WHL export_bins = [:NEM] export_rates = Dict(:NEM => nem_rate) - if sum(exc_rate) < 0 + if sum(exc_rate) < 0 # NEM with EXC rate push!(export_bins, :EXC) export_rates[:EXC] = exc_rate end - else + else # NEM and WHL export_bins = [:NEM, :WHL] export_rates = Dict(:NEM => nem_rate, :WHL => whl_rate) - if sum(exc_rate) < 0 + if sum(exc_rate) < 0 # NEM and WHL with EXC rate push!(export_bins, :EXC) export_rates[:EXC] = exc_rate end diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index b28f3e9c0..b5eda88fe 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -31,7 +31,7 @@ `ElectricUtility` is an optional REopt input with the following keys and default values: ```julia net_metering_limit_kw::Real = 0, - interconnection_limit_kw::Real = 1.0e9, + interconnection_limit_kw::Real = 1.0e9, # Limit on total electric system capacity size that can be interconnected to the grid outage_start_time_step::Int=0, # for modeling a single outage, with critical load spliced into the baseline load ... outage_end_time_step::Int=0, # ... utiltity production_factor = 0 during the outage allow_simultaneous_export_import::Bool = true, # if true the site has two meters (in effect) @@ -40,23 +40,16 @@ outage_start_time_steps::Array{Int,1}=Int[], # we minimize the maximum outage cost over outage start times outage_durations::Array{Int,1}=Int[], # one-to-one with outage_probabilities, outage_durations can be a random variable outage_probabilities::Array{R,1} where R<:Real = [1.0], - outage_time_steps::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:maximum(outage_durations), - scenarios::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:length(outage_durations), # Emissions and renewable energy inputs: emissions_region::String = "", # AVERT emissions region. Default is based on location, or can be overriden by providing region here. emissions_factor_series_lb_CO2_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # can be scalar or timeseries (aligned with time_steps_per_hour) emissions_factor_series_lb_NOx_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # can be scalar or timeseries (aligned with time_steps_per_hour) emissions_factor_series_lb_SO2_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # can be scalar or timeseries (aligned with time_steps_per_hour) emissions_factor_series_lb_PM25_per_kwh::Union{Real,Array{<:Real,1}} = Float64[], # can be scalar or timeseries (aligned with time_steps_per_hour) - emissions_factor_CO2_decrease_fraction::Real = 0.01174, + emissions_factor_CO2_decrease_fraction::Real = 0.01174, # Annual percent decrease in the total annual CO2 emissions rate of the grid. A negative value indicates an annual increase. emissions_factor_NOx_decrease_fraction::Real = 0.01174, emissions_factor_SO2_decrease_fraction::Real = 0.01174, - emissions_factor_PM25_decrease_fraction::Real = 0.01174, - # fields from other models needed for validation - CO2_emissions_reduction_min_fraction::Union{Real, Nothing} = nothing, # passed from Site - CO2_emissions_reduction_max_fraction::Union{Real, Nothing} = nothing, # passed from Site - include_climate_in_objective::Bool = false, # passed from Settings - include_health_in_objective::Bool = false # passed from Settings + emissions_factor_PM25_decrease_fraction::Real = 0.01174 ``` !!! note "Outage modeling" @@ -122,7 +115,7 @@ struct ElectricUtility allow_simultaneous_export_import::Bool=true, # if true the site has two meters (in effect) # next 5 variables below used for minimax the expected outage cost, # with max taken over outage start time, expectation taken over outage duration - outage_start_time_steps::Array{Int,1}=Int[], # we minimize the maximum outage cost over outage start times + outage_start_time_steps::Array{Int,1}=Int[], # we include in the minimization the maximum outage cost over outage start times outage_durations::Array{Int,1}=Int[], # one-to-one with outage_probabilities, outage_durations can be a random variable outage_probabilities::Array{<:Real,1} = isempty(outage_durations) ? Float64[] : [1/length(outage_durations) for p_i in 1:length(outage_durations)], outage_time_steps::Union{Nothing, UnitRange} = isempty(outage_durations) ? nothing : 1:maximum(outage_durations), @@ -207,6 +200,12 @@ struct ElectricUtility emissions and renewable energy percentage calculations and constraints do not consider outages." end end + if length(outage_durations) != length(outage_probabilities) + throw(@error("ElectricUtility inputs outage_durations and outage_probabilities must be the same length")) + end + if length(outage_probabilities) >= 1 && (sum(outage_probabilities) < 0.99999 || sum(outage_probabilities) > 1.00001) + throw(@error("Sum of ElectricUtility inputs outage_probabilities must be equal to 1")) + end new( is_MPC ? "" : emissions_region, diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 692ffa961..0784baaee 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -183,9 +183,9 @@ end inverter_replacement_year::Int = 10 battery_replacement_year::Int = 10 macrs_option_years::Int = 7 - macrs_bonus_fraction::Float64 = 1.0 + macrs_bonus_fraction::Float64 = 0.8 macrs_itc_reduction::Float64 = 0.5 - total_itc_fraction::Float64 = 0.0 + total_itc_fraction::Float64 = 0.3 total_rebate_per_kw::Real = 0.0 total_rebate_per_kwh::Real = 0.0 charge_efficiency::Float64 = rectifier_efficiency_fraction * internal_efficiency_fraction^0.5 @@ -217,9 +217,9 @@ Base.@kwdef struct ElectricStorageDefaults inverter_replacement_year::Int = 10 battery_replacement_year::Int = 10 macrs_option_years::Int = 7 - macrs_bonus_fraction::Float64 = 1.0 + macrs_bonus_fraction::Float64 = 0.8 macrs_itc_reduction::Float64 = 0.5 - total_itc_fraction::Float64 = 0.0 + total_itc_fraction::Float64 = 0.3 total_rebate_per_kw::Real = 0.0 total_rebate_per_kwh::Real = 0.0 charge_efficiency::Float64 = rectifier_efficiency_fraction * internal_efficiency_fraction^0.5 diff --git a/src/core/energy_storage/thermal_storage.jl b/src/core/energy_storage/thermal_storage.jl index af2d2241e..dd75d09de 100644 --- a/src/core/energy_storage/thermal_storage.jl +++ b/src/core/energy_storage/thermal_storage.jl @@ -37,18 +37,18 @@ Cold thermal energy storage sytem; specifically, a chilled water system used to ```julia min_gal::Float64 = 0.0 max_gal::Float64 = 0.0 - hot_water_temp_degF::Float64 = 56.0 - cool_water_temp_degF::Float64 = 44.0 - internal_efficiency_fraction::Float64 = 0.999999 - soc_min_fraction::Float64 = 0.1 - soc_init_fraction::Float64 = 0.5 - installed_cost_per_gal::Float64 = 1.50 - thermal_decay_rate_fraction::Float64 = 0.0004 - om_cost_per_gal::Float64 = 0.0 - macrs_option_years::Int = 0 - macrs_bonus_fraction::Float64 = 0.0 - macrs_itc_reduction::Float64 = 0.0 - total_itc_fraction::Float64 = 0.0 + hot_water_temp_degF::Float64 = 56.0 # Warmed-side return water temperature from the cooling load to the ColdTES (top of tank) + cool_water_temp_degF::Float64 = 44.0 # Chilled-side supply water temperature from ColdTES (bottom of tank) to the cooling load + internal_efficiency_fraction::Float64 = 0.999999 # Thermal losses due to mixing from thermal power entering or leaving tank + soc_min_fraction::Float64 = 0.1 # Minimum allowable TES thermal state of charge + soc_init_fraction::Float64 = 0.5 # TES thermal state of charge at first hour of optimization + installed_cost_per_gal::Float64 = 1.50 # Thermal energy-based cost of TES (e.g. volume of the tank) + thermal_decay_rate_fraction::Float64 = 0.0004 # Thermal loss (gain) rate as a fraction of energy storage capacity, per hour (frac*energy_capacity/hr = kw_thermal) + om_cost_per_gal::Float64 = 0.0 # Yearly fixed O&M cost dependent on storage energy size + macrs_option_years::Int = 7 + macrs_bonus_fraction::Float64 = 0.8 + macrs_itc_reduction::Float64 = 0.5 + total_itc_fraction::Float64 = 0.3 total_rebate_per_kwh::Float64 = 0.0 ``` """ @@ -63,10 +63,10 @@ Base.@kwdef struct ColdThermalStorageDefaults <: AbstractThermalStorageDefaults installed_cost_per_gal::Float64 = 1.50 thermal_decay_rate_fraction::Float64 = 0.0004 om_cost_per_gal::Float64 = 0.0 - macrs_option_years::Int = 0 - macrs_bonus_fraction::Float64 = 0.0 - macrs_itc_reduction::Float64 = 0.0 - total_itc_fraction::Float64 = 0.0 + macrs_option_years::Int = 7 + macrs_bonus_fraction::Float64 = 0.8 + macrs_itc_reduction::Float64 = 0.5 + total_itc_fraction::Float64 = 0.3 total_rebate_per_kwh::Float64 = 0.0 end @@ -85,10 +85,10 @@ end installed_cost_per_gal::Float64 = 1.50 thermal_decay_rate_fraction::Float64 = 0.0004 om_cost_per_gal::Float64 = 0.0 - macrs_option_years::Int = 0 - macrs_bonus_fraction::Float64 = 0.0 - macrs_itc_reduction::Float64 = 0.0 - total_itc_fraction::Float64 = 0.0 + macrs_option_years::Int = 7 + macrs_bonus_fraction::Float64 = 0.8 + macrs_itc_reduction::Float64 = 0.5 + total_itc_fraction::Float64 = 0.3 total_rebate_per_kwh::Float64 = 0.0 ``` """ @@ -103,10 +103,10 @@ Base.@kwdef struct HotThermalStorageDefaults <: AbstractThermalStorageDefaults installed_cost_per_gal::Float64 = 1.50 thermal_decay_rate_fraction::Float64 = 0.0004 om_cost_per_gal::Float64 = 0.0 - macrs_option_years::Int = 0 - macrs_bonus_fraction::Float64 = 0.0 - macrs_itc_reduction::Float64 = 0.0 - total_itc_fraction::Float64 = 0.0 + macrs_option_years::Int = 7 + macrs_bonus_fraction::Float64 = 0.8 + macrs_itc_reduction::Float64 = 0.5 + total_itc_fraction::Float64 = 0.3 total_rebate_per_kwh::Float64 = 0.0 end diff --git a/src/core/existing_boiler.jl b/src/core/existing_boiler.jl index dcaf39788..db425301d 100644 --- a/src/core/existing_boiler.jl +++ b/src/core/existing_boiler.jl @@ -50,10 +50,10 @@ end """ `ExistingBoiler` is an optional REopt input with the following keys and default values: ```julia - max_heat_demand_kw::Real=0, - production_type::String = "hot_water", + max_heat_demand_kw::Real=0, # Auto-populated based on SpaceHeatingLoad and DomesticHotWaterLoad inputs + production_type::String = "hot_water", # Can be "steam" or "hot_water" max_thermal_factor_on_peak_load::Real = 1.25, - efficiency::Real = NaN, + efficiency::Real = NaN, # Existing boiler system efficiency - conversion of fuel to usable heating thermal energy. See note below. 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, diff --git a/src/core/financial.jl b/src/core/financial.jl index 26e87f4d4..5c7cbf6b6 100644 --- a/src/core/financial.jl +++ b/src/core/financial.jl @@ -42,7 +42,7 @@ owner_tax_rate_fraction::Real = 0.26, 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, + value_of_lost_load_per_kwh::Union{Array{R,1}, R} where R<:Real = 1.00, #only applies to multiple outage modeling 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], @@ -59,11 +59,7 @@ PM25_onsite_fuelburn_cost_per_tonne::Union{Nothing,Real} = nothing, NOx_cost_escalation_rate_fraction::Union{Nothing,Real} = nothing, SO2_cost_escalation_rate_fraction::Union{Nothing,Real} = nothing, - PM25_cost_escalation_rate_fraction::Union{Nothing,Real} = nothing, - # fields from other models needed for validation - latitude::Real, # Passed from Site - longitude::Real, # Passed from Site - include_health_in_objective::Bool = false # Passed from Settings + PM25_cost_escalation_rate_fraction::Union{Nothing,Real} = nothing ``` !!! note "Third party financing" @@ -120,7 +116,7 @@ struct Financial owner_tax_rate_fraction::Real = 0.26, owner_discount_rate_fraction::Real = 0.0564, analysis_years::Int = 25, - value_of_lost_load_per_kwh::Union{Array{<:Real,1}, Real} = 1.00, + value_of_lost_load_per_kwh::Union{Array{<:Real,1}, Real} = 1.00, #only applies to multiple outage modeling 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], diff --git a/src/core/ghp.jl b/src/core/ghp.jl index 7284dd4fd..47b60b702 100644 --- a/src/core/ghp.jl +++ b/src/core/ghp.jl @@ -52,9 +52,9 @@ struct with outer constructor: can_serve_dhw::Bool = false macrs_option_years::Int = 5 - macrs_bonus_fraction::Float64 = 1.0 + macrs_bonus_fraction::Float64 = 0.8 macrs_itc_reduction::Float64 = 0.5 - federal_itc_fraction::Float64 = 0.1 + federal_itc_fraction::Float64 = 0.3 federal_rebate_per_ton::Float64 = 0.0 federal_rebate_per_kw::Float64 = 0.0 state_ibi_fraction::Float64 = 0.0 @@ -98,9 +98,9 @@ Base.@kwdef mutable struct GHP <: AbstractGHP can_serve_dhw::Bool = false macrs_option_years::Int = 5 - macrs_bonus_fraction::Float64 = 1.0 + macrs_bonus_fraction::Float64 = 0.8 macrs_itc_reduction::Float64 = 0.5 - federal_itc_fraction::Float64 = 0.1 + federal_itc_fraction::Float64 = 0.3 federal_rebate_per_ton::Float64 = 0.0 federal_rebate_per_kw::Float64 = 0.0 state_ibi_fraction::Float64 = 0.0 diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index 4af6c7657..bb50a944a 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -237,20 +237,19 @@ function get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_ end """ - function CoolingLoad(; - doe_reference_name::String = "", - city::String = "", - blended_doe_reference_names::Array{String, 1} = String[], - blended_doe_reference_percents::Array{<:Real,1} = Real[], - annual_tonhour::Union{Real, Nothing} = nothing, - monthly_tonhour::Array{<:Real,1} = Real[], - thermal_loads_ton::Array{<:Real,1} = Real[], # Vector of cooling thermal loads [ton] = [short ton hours/hour]. Length must equal 8760 * `Settings.time_steps_per_hour` - annual_fraction_of_electric_load::Union{Real, Nothing} = nothing, # Fraction of total electric load that is used for cooling - monthly_fractions_of_electric_load::Array{<:Real,1} = Real[], - per_time_step_fractions_of_electric_load::Array{<:Real,1} = Real[], - existing_chiller_cop::Real = nothing, - existing_chiller_max_thermal_factor_on_peak_load::Real= nothing - ) +`CoolingLoad` is an optional REopt input with the following keys and default values: +```julia + doe_reference_name::String = "", + city::String = "", + blended_doe_reference_names::Array{String, 1} = String[], + blended_doe_reference_percents::Array{<:Real,1} = Real[], + annual_tonhour::Union{Real, Nothing} = nothing, + monthly_tonhour::Array{<:Real,1} = Real[], + thermal_loads_ton::Array{<:Real,1} = Real[], # Vector of cooling thermal loads [ton] = [short ton hours/hour]. Length must equal 8760 * `Settings.time_steps_per_hour` + annual_fraction_of_electric_load::Union{Real, Nothing} = nothing, # Fraction of total electric load that is used for cooling + monthly_fractions_of_electric_load::Array{<:Real,1} = Real[], + per_time_step_fractions_of_electric_load::Array{<:Real,1} = Real[] +``` There are many ways to define a `CoolingLoad`: diff --git a/src/core/pv.jl b/src/core/pv.jl index ca1bd5b85..89f8a685b 100644 --- a/src/core/pv.jl +++ b/src/core/pv.jl @@ -46,14 +46,14 @@ om_cost_per_kw::Real=17.0, degradation_fraction::Real=0.005, macrs_option_years::Int = 5, - macrs_bonus_fraction::Real = 1.0, + macrs_bonus_fraction::Real = 0.8, macrs_itc_reduction::Real = 0.5, kw_per_square_foot::Real=0.01, acres_per_kw::Real=6e-3, inv_eff::Real=0.96, dc_ac_ratio::Real=1.2, production_factor_series::Union{Nothing, Array{<:Real,1}} = nothing, - federal_itc_fraction::Real = 0.26, + federal_itc_fraction::Real = 0.3, federal_rebate_per_kw::Real = 0.0, state_ibi_fraction::Real = 0.0, state_ibi_max::Real = 1.0e10, @@ -145,14 +145,14 @@ struct PV <: AbstractTech om_cost_per_kw::Real=17.0, degradation_fraction::Real=0.005, macrs_option_years::Int = 5, - macrs_bonus_fraction::Real = 1.0, + macrs_bonus_fraction::Real = 0.8, macrs_itc_reduction::Real = 0.5, kw_per_square_foot::Real=0.01, acres_per_kw::Real=6e-3, inv_eff::Real=0.96, dc_ac_ratio::Real=1.2, production_factor_series::Union{Nothing, Array{Real,1}} = nothing, - federal_itc_fraction::Real = 0.26, + federal_itc_fraction::Real = 0.3, federal_rebate_per_kw::Real = 0.0, state_ibi_fraction::Real = 0.0, state_ibi_max::Real = 1.0e10, diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 244cbc293..0ee62eb3e 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -471,6 +471,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:TotalProductionIncentive] * (1 - p.s.financial.owner_tax_rate_fraction) + # Comfort limit violation costs + #TODO: add this to objective like SOC incentive below and + #don't then subtract out when setting lcc in results/financial.jl m[:dvComfortLimitViolationCost] + # Additional annual costs, tax deductible for owner (only applies when `off_grid_flag` is true) @@ -492,16 +494,37 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_to_expression!(Costs, m[:Lifecycle_Emissions_Cost_Health]) end - @objective(m, Min, m[:Costs]) - - if !(isempty(p.s.storage.types.elec)) && p.s.settings.add_soc_incentive # Keep SOC high - @objective(m, Min, m[:Costs] - - sum(m[:dvStoredEnergy][b, ts] for b in p.s.storage.types.elec, ts in p.time_steps) / - (8760. / p.hours_per_time_step) + @expression(m, Objective, + m[:Costs] + ) + + if !(isempty(p.s.storage.types.elec)) && p.s.settings.add_soc_incentive + # Incentive to keep SOC high + add_to_expression!( + Objective, + - sum( + m[:dvStoredEnergy][b, ts] for b in p.s.storage.types.elec, ts in p.time_steps + ) / (8760. / p.hours_per_time_step) + ) + end + if !isempty(p.s.electric_utility.outage_durations) + # Incentive to minimize unserved load in each outage, not just the max over outage start times + add_to_expression!( + Objective, + sum(sum(0.0001 * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) for s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps) ) - end + @objective(m, Min, m[:Objective]) + + # if !(isempty(p.s.storage.types.elec)) && p.s.settings.add_soc_incentive # Keep SOC high + # @objective(m, Min, m[:Costs] - + # sum(m[:dvStoredEnergy][b, ts] for b in p.s.storage.types.elec, ts in p.time_steps) / + # (8760. / p.hours_per_time_step) + # ) + + # end + for b in p.s.storage.types.elec if p.s.storage.attr[b].model_degradation add_degradation(m, p; b=b) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index e3fbf96d1..d80bc028a 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -547,6 +547,21 @@ function setup_wind_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_s max_sizes["Wind"] = s.wind.max_kw min_sizes["Wind"] = s.wind.min_kw existing_sizes["Wind"] = 0.0 + + if !(s.site.land_acres === nothing) # Limit based on available land + land_max_kw = s.site.land_acres / s.wind.acres_per_kw + if land_max_kw < 1500 # turbines less than 1.5 MW aren't subject to the acres/kW limit + land_max_kw = 1500 + end + if max_sizes["Wind"] > land_max_kw # if user-provided max is greater than land max, update max (otherwise use user-provided max) + @warn "User-provided maximum wind kW is greater than the calculated land-constrained kW (site.land_acres/wind.acres_per_kw). Wind max kW has been updated to land-constrained max of $(land_max_kw) kW." + max_sizes["Wind"] = land_max_kw + end + if min_sizes["Wind"] > max_sizes["Wind"] # If user-provided min is greater than max (updated to land max as above), send error + throw(@error("User-provided minimum wind kW is greater than either wind.max_kw or calculated land-constrained kW (site.land_acres/wind.acres_per_kw). Update wind.min_kw or site.land_acres")) + end + end + update_cost_curve!(s.wind, "Wind", s.financial, cap_cost_slope, segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint ) diff --git a/src/core/reopt_multinode.jl b/src/core/reopt_multinode.jl index 80be97166..240956f6e 100644 --- a/src/core/reopt_multinode.jl +++ b/src/core/reopt_multinode.jl @@ -89,6 +89,7 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T} ################################# Objective Function ######################################## m[Symbol("Costs"*_n)] = @expression(m, + #TODO: update in line with non-multinode version # Capital Costs m[Symbol("TotalTechCapCosts"*_n)] + m[Symbol("TotalStorageCapCosts"*_n)] + diff --git a/src/core/site.jl b/src/core/site.jl index 11af01fef..2907ae879 100644 --- a/src/core/site.jl +++ b/src/core/site.jl @@ -34,14 +34,14 @@ Inputs related to the physical location: ```julia latitude::Real, longitude::Real, - land_acres::Union{Real, Nothing} = nothing, + land_acres::Union{Real, Nothing} = nothing, # acres of land available for PV panels and/or Wind turbines. Constraint applied separately to PV and Wind, meaning the two technologies are assumed to be able to be co-located. roof_squarefeet::Union{Real, Nothing} = nothing, min_resil_time_steps::Int=0, mg_tech_sizes_equal_grid_sizes::Bool = true, node::Int = 1, CO2_emissions_reduction_min_fraction::Union{Float64, Nothing} = nothing, CO2_emissions_reduction_max_fraction::Union{Float64, Nothing} = nothing, - bau_emissions_lb_CO2_per_year::Union{Float64, Nothing} = nothing, + bau_emissions_lb_CO2_per_year::Union{Float64, Nothing} = nothing, # Auto-populated based on BAU run. This input will be overwritten if the BAU scenario is run, but can be user-provided if no BAU scenario is run. bau_grid_emissions_lb_CO2_per_year::Union{Float64, Nothing} = nothing, renewable_electricity_min_fraction::Real = 0.0, renewable_electricity_max_fraction::Union{Float64, Nothing} = nothing, diff --git a/src/core/urdb.jl b/src/core/urdb.jl index 87755f368..41cbbefe5 100644 --- a/src/core/urdb.jl +++ b/src/core/urdb.jl @@ -63,24 +63,24 @@ end """ - URDBrate(urdb_label::String, year::Int=2019; time_steps_per_hour=1) + URDBrate(urdb_label::String, year::Int; time_steps_per_hour=1) download URDB dict, parse into reopt inputs, return URDBrate struct. year is required to align weekday/weekend schedules. """ -function URDBrate(urdb_label::String, year::Int=2019; time_steps_per_hour=1) +function URDBrate(urdb_label::String, year::Int; time_steps_per_hour=1) urdb_response = download_urdb(urdb_label) URDBrate(urdb_response, year; time_steps_per_hour=time_steps_per_hour) end """ - URDBrate(util_name::String, rate_name::String, year::Int=2019; time_steps_per_hour=1) + URDBrate(util_name::String, rate_name::String, year::Int; time_steps_per_hour=1) download URDB dict, parse into reopt inputs, return URDBrate struct. year is required to align weekday/weekend schedules. """ -function URDBrate(util_name::String, rate_name::String, year::Int=2019; time_steps_per_hour=1) +function URDBrate(util_name::String, rate_name::String, year::Int; time_steps_per_hour=1) urdb_response = download_urdb(util_name, rate_name) URDBrate(urdb_response, year; time_steps_per_hour=time_steps_per_hour) end @@ -92,7 +92,7 @@ end process URDB response dict, parse into reopt inputs, return URDBrate struct. year is required to align weekday/weekend schedules. """ -function URDBrate(urdb_response::Dict, year::Int=2019; time_steps_per_hour=1) +function URDBrate(urdb_response::Dict, year::Int; time_steps_per_hour=1) demand_min = get(urdb_response, "peakkwcapacitymin", 0.0) # TODO add check for site min demand against tariff? diff --git a/src/core/wind.jl b/src/core/wind.jl index a34703161..636165ad2 100644 --- a/src/core/wind.jl +++ b/src/core/wind.jl @@ -40,10 +40,11 @@ wind_direction_degrees = [], temperature_celsius = [], pressure_atmospheres = [], + acres_per_kw = 0.03, # assuming a power density of 30 acres per MW for turbine sizes >= 1.5 MW. No size constraint applied to turbines below 1.5 MW capacity. (not exposed in API) macrs_option_years = 5, - macrs_bonus_fraction = 0.0, + macrs_bonus_fraction = 0.8, macrs_itc_reduction = 0.5, - federal_itc_fraction = nothing, + federal_itc_fraction = 0.3, federal_rebate_per_kw = 0.0, state_ibi_fraction = 0.0, state_ibi_max = 1.0e10, @@ -62,37 +63,30 @@ 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. ``` +!!! note "Default assumptions" + `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. -`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. - -If no `installed_cost_per_kw` is provided then it is determined from: -```julia -size_class_to_installed_cost = Dict( - "residential"=> 11950.0, - "commercial"=> 7390.0, - "medium"=> 4440.0, - "large"=> 3450.0 -) -``` - -The Federal Investment Tax Credit is adjusted based on the `size_class` as follows (if the default of 0.3 is not changed): -```julia -size_class_to_itc_incentives = Dict( - "residential"=> 0.3, - "commercial"=> 0.3, - "medium"=> 0.12, - "large"=> 0.12 -) -``` - -If the `production_factor_series` is not provided then NREL's System Advisor Model (SAM) is used to get the wind turbine -production factor. - -Wind resource values are optional, i.e. -(`wind_meters_per_sec`, `wind_direction_degrees`, `temperature_celsius`, and `pressure_atmospheres`). -If not provided then the resource values are downloaded from NREL's Wind Toolkit. -These values are passed to SAM to get the turbine production factor. + If no `installed_cost_per_kw` is provided then it is determined from: + ```julia + size_class_to_installed_cost = Dict( + "residential"=> 11950.0, + "commercial"=> 7390.0, + "medium"=> 4440.0, + "large"=> 3450.0 + ) + ``` + If the `production_factor_series` is not provided then NREL's System Advisor Model (SAM) is used to get the wind turbine + production factor. +!!! note "Wind resource value inputs" + Wind resource values are optional (i.e., `wind_meters_per_sec`, `wind_direction_degrees`, `temperature_celsius`, and `pressure_atmospheres`). + If not provided then the resource values are downloaded from NREL's Wind Toolkit. + These values are passed to SAM to get the turbine production factor. + +!!! note "Wind sizing and land constraint" + Wind size is constrained by Site.land_acres, assuming a power density of Wind.acres_per_kw for turbine sizes above 1.5 MW (default assumption of 30 acres per MW). + If the turbine size recommended is smaller than 1.5 MW, the input for land available will not constrain the system size. + If the the land available constrains the system size to less than 1.5 MW, the system will be capped at 1.5 MW (i.e., turbines < 1.5 MW are not subject to the acres/kW limit). """ struct Wind <: AbstractTech @@ -107,10 +101,11 @@ struct Wind <: AbstractTech wind_direction_degrees::AbstractArray{Float64,1} temperature_celsius::AbstractArray{Float64,1} pressure_atmospheres::AbstractArray{Float64,1} + acres_per_kw::Real macrs_option_years::Int macrs_bonus_fraction::Real macrs_itc_reduction::Real - federal_itc_fraction::Union{Nothing, Real} + federal_itc_fraction::Real federal_rebate_per_kw::Real state_ibi_fraction::Real state_ibi_max::Real @@ -142,10 +137,11 @@ struct Wind <: AbstractTech wind_direction_degrees = [], temperature_celsius = [], pressure_atmospheres = [], + acres_per_kw = 0.03, # assuming a power density of 30 acres per MW for turbine sizes >= 1.5 MW. No size constraint applied to turbines below 1.5 MW capacity. macrs_option_years = 5, - macrs_bonus_fraction = 0.0, + macrs_bonus_fraction = 0.8, macrs_itc_reduction = 0.5, - federal_itc_fraction = nothing, + federal_itc_fraction = 0.3, federal_rebate_per_kw = 0.0, state_ibi_fraction = 0.0, state_ibi_max = 1.0e10, @@ -178,13 +174,6 @@ struct Wind <: AbstractTech "medium"=> 2766.0, "large"=> 2239.0 ) - - size_class_to_itc_incentives = Dict( - "residential"=> 0.3, - "commercial"=> 0.3, - "medium"=> 0.12, - "large"=> 0.12 - ) if size_class == "" if average_elec_load <= 12.5 @@ -204,10 +193,6 @@ struct Wind <: AbstractTech installed_cost_per_kw = size_class_to_installed_cost[size_class] end - if isnothing(federal_itc_fraction) - federal_itc_fraction = size_class_to_itc_incentives[size_class] - end - hub_height = size_class_to_hub_height[size_class] if !(off_grid_flag) && !(operating_reserve_required_fraction == 0.0) @@ -234,6 +219,7 @@ struct Wind <: AbstractTech wind_direction_degrees, temperature_celsius, pressure_atmospheres, + acres_per_kw, macrs_option_years, macrs_bonus_fraction, macrs_itc_reduction, diff --git a/src/results/electric_utility.jl b/src/results/electric_utility.jl index 236b6193f..7eb0ef6ee 100644 --- a/src/results/electric_utility.jl +++ b/src/results/electric_utility.jl @@ -47,6 +47,10 @@ REopt performs load balances using average annual production values for technologies that include degradation. Therefore, all timeseries (`_series`) and `annual_` results should be interpretted as energy and emissions outputs averaged over the analysis period. +!!! note "Emissions outputs" + By default, REopt uses marginal emissions rates for grid-purchased electricity. Marginal emissions rates are most appropriate for reporting a change in emissions (avoided or increased) rather than emissions totals. + It is therefore recommended that emissions results from REopt (using default marginal emissions rates) be reported as the difference in emissions between the optimized and BAU case. + """ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, d::Dict; _n="") # Adds the `ElectricUtility` results to the dictionary passed back from `run_reopt` using the solved model `m` and the `REoptInputs` for node `_n`. diff --git a/src/results/existing_chiller.jl b/src/results/existing_chiller.jl index 0d4ef7b35..f21680a47 100644 --- a/src/results/existing_chiller.jl +++ b/src/results/existing_chiller.jl @@ -31,7 +31,7 @@ `ExistingChiller` results keys: - `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage - `thermal_to_load_series_ton` # Thermal production to cooling load -- `year_one_electric_consumption_series` +- `electric_consumption_series_kw` - `annual_electric_consumption_kwh` - `annual_thermal_production_tonhour` diff --git a/src/results/heating_cooling_load.jl b/src/results/heating_cooling_load.jl index 6bab629b9..4a6baf7d1 100644 --- a/src/results/heating_cooling_load.jl +++ b/src/results/heating_cooling_load.jl @@ -29,8 +29,10 @@ # ********************************************************************************* """ `CoolingLoad` results keys: -- `load_series_ton` vector of site cooling load in every time step -- `annual_calculated_tonhour` sum of the `load_series_ton` +- `load_series_ton` # vector of site cooling load in every time step +- `annual_calculated_tonhour` # sum of the `load_series_ton`. Annual site total cooling load [tonhr] +- `electric_chiller_base_load_series_kw` # Hourly total base load drawn from chiller [kW-electric] +- `annual_electric_chiller_base_load_kwh` # Annual total base load drawn from chiller [kWh-electric] !!! note "'Series' and 'Annual' energy outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. @@ -68,6 +70,9 @@ end - `annual_calculated_dhw_thermal_load_mmbtu` sum of the `dhw_load_series_mmbtu_per_hour` - `annual_calculated_space_heating_thermal_load_mmbtu` sum of the `space_heating_thermal_load_series_mmbtu_per_hour` - `annual_calculated_total_heating_thermal_load_mmbtu` sum of the `total_heating_thermal_load_series_mmbtu_per_hour` +- `annual_calculated_dhw_boiler_fuel_load_mmbtu` +- `annual_calculated_space_heating_boiler_fuel_load_mmbtu` +- `annual_calculated_total_heating_boiler_fuel_load_mmbtu` """ function add_heating_load_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") # Adds the `ElectricLoad` results to the dictionary passed back from `run_reopt` using the solved model `m` and the `REoptInputs` for node `_n`. diff --git a/src/results/outages.jl b/src/results/outages.jl index 36fde2f3a..527b337e9 100644 --- a/src/results/outages.jl +++ b/src/results/outages.jl @@ -30,23 +30,27 @@ """ `Outages` results keys: - `expected_outage_cost` The expected outage cost over the random outages modeled. -- `max_outage_cost_per_outage_duration_series` The maximum outage cost in every outage duration modeled. +- `max_outage_cost_per_outage_duration` The maximum outage cost in every outage duration modeled. - `unserved_load_series` The amount of unserved load in each outage and each time step. -- `unserved_load_per_outage_series` The total unserved load in each outage. +- `unserved_load_per_outage` The total unserved load in each outage. - `mg_storage_upgrade_cost` The cost to include the storage system in the microgrid. - `storage_upgraded` Boolean that is true if it is cost optimal to include the storage system in the microgrid. -= `discharge_from_storage_series` Array of storage power discharged in every outage modeled. -- `PVmg_kw` Optimal microgrid PV capacity. Note that the name `PV` can change based on user provided `PV.name`. +- `discharge_from_storage_series` Array of storage power discharged in every outage modeled. +- `PV_mg_kw` Optimal microgrid PV capacity. Note that the name `PV` can change based on user provided `PV.name`. +- `PV_upgraded` Boolean that is true if it is cost optimal to include the PV system in the microgrid. - `mg_PV_upgrade_cost` The cost to include the PV system in the microgrid. -- `mgPV_to_storage_series` Array of PV power sent to the battery in every outage modeled. -- `mgPV_curtailed_series` Array of PV curtailed in every outage modeled. -- `mgPV_to_load_series` Array of PV power used to meet load in every outage modeled. -- `Generatormg_kw` Optimal microgrid Generator capacity. Note that the name `Generator` can change based on user provided `Generator.name`. +- `mg_PV_to_storage_series` Array of PV power sent to the battery in every outage modeled. +- `mg_PV_curtailed_series` Array of PV curtailed in every outage modeled. +- `mg_PV_to_load_series` Array of PV power used to meet load in every outage modeled. +- `Generator_mg_kw` Optimal microgrid Generator capacity. Note that the name `Generator` can change based on user provided `Generator.name`. +- `Generator_upgraded` Boolean that is true if it is cost optimal to include the Generator in the microgrid. - `mg_Generator_upgrade_cost` The cost to include the Generator system in the microgrid. -- `mgGenerator_to_storage_series` Array of Generator power sent to the battery in every outage modeled. -- `mgGenerator_curtailed_series` Array of Generator curtailed in every outage modeled. -- `mgGenerator_to_load_series` Array of Generator power used to meet load in every outage modeled. -- `mg_Generator_fuel_used` Array of Generator fuel used in every outage modeled. +- `mg_Generator_to_storage_series` Array of Generator power sent to the battery in every outage modeled. +- `mg_Generator_curtailed_series` Array of Generator curtailed in every outage modeled. +- `mg_Generator_to_load_series` Array of Generator power used to meet load in every outage modeled. +- `mg_Generator_fuel_used_per_outage` Array of Generator fuel used in every outage modeled. +- `generator_fuel_used_per_outage` Array of fuel used in every outage modeled, summed over all Generators. +- `microgrid_upgrade_capital_cost` Total capital cost of including technologies in the microgrid !!! warn The output keys for "Outages" are subject to change. @@ -71,18 +75,24 @@ function add_outage_results(m, p, d::Dict) # other results. r = Dict{String, Any}() r["expected_outage_cost"] = value(m[:ExpectedOutageCost]) - r["max_outage_cost_per_outage_duration_series"] = value.(m[:dvMaxOutageCost]).data + r["max_outage_cost_per_outage_duration"] = value.(m[:dvMaxOutageCost]).data r["unserved_load_series"] = value.(m[:dvUnservedLoad]).data S = length(p.s.electric_utility.scenarios) T = length(p.s.electric_utility.outage_start_time_steps) + TS = length(p.s.electric_utility.outage_time_steps) unserved_load_per_outage = Array{Float64}(undef, S, T) for s in 1:S, t in 1:T + if p.s.electric_utility.outage_durations[s] < TS + r["unserved_load_series"][s,t,p.s.electric_utility.outage_durations[s]+1:end] .= 0 + end unserved_load_per_outage[s, t] = sum(r["unserved_load_series"][s, t, ts] for ts in 1:p.s.electric_utility.outage_durations[s]) - # need the ts in 1:p.s.electric_utility.outage_durations[s] b/c dvUnservedLoad has unused values in third dimension + # need to sum over ts in 1:p.s.electric_utility.outage_durations[s] + # instead of all ts b/c dvUnservedLoad has unused values in third dimension end - r["unserved_load_per_outage_series"] = round.(unserved_load_per_outage, digits=2) + r["unserved_load_per_outage"] = round.(unserved_load_per_outage, digits=2) r["mg_storage_upgrade_cost"] = value(m[:dvMGStorageUpgradeCost]) + r["microgrid_upgrade_capital_cost"] = r["mg_storage_upgrade_cost"] r["discharge_from_storage_series"] = value.(m[:dvMGDischargeFromStorage]).data for t in p.techs.all @@ -96,11 +106,12 @@ function add_outage_results(m, p, d::Dict) # need the following logic b/c can have non-zero mg capacity when not using the capacity # due to the constraint for setting the mg capacities equal to the grid connected capacities if Bool(r[t * "_upgraded"]) - r[string(t, "mg_kw")] = round(value(m[:dvMGsize][t]), digits=4) + r[string(t, "_mg_kw")] = round(value(m[:dvMGsize][t]), digits=4) else - r[string(t, "mg_kw")] = 0 + r[string(t, "_mg_kw")] = 0 end r[string("mg_", t, "_upgrade_cost")] = round(value(m[:dvMGTechUpgradeCost][t]), digits=2) + r["microgrid_upgrade_capital_cost"] += r[string("mg_", t, "_upgrade_cost")] if !isempty(p.s.storage.types.elec) PVtoBatt = (m[:dvMGProductionToStorage][t, s, tz, ts] for @@ -110,16 +121,16 @@ function add_outage_results(m, p, d::Dict) else PVtoBatt = [] end - r[string("mg", t, "_to_storage_series")] = round.(value.(PVtoBatt), digits=3) + r[string("mg_", t, "_to_storage_series")] = round.(value.(PVtoBatt), digits=3) PVtoCUR = (m[:dvMGCurtail][t, s, tz, ts] for 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) - r[string("mg", t, "_curtailed_series")] = round.(value.(PVtoCUR), digits=3) + r[string("mg_", t, "_curtailed_series")] = round.(value.(PVtoCUR), digits=3) PVtoLoad = ( - m[:dvMGRatedProduction][t, s, tz, ts] * p.production_factor[t, tz+ts] + m[:dvMGRatedProduction][t, s, tz, ts] * p.production_factor[t, tz+ts-1] * p.levelization_factor[t] - m[:dvMGCurtail][t, s, tz, ts] - m[:dvMGProductionToStorage][t, s, tz, ts] for @@ -127,7 +138,7 @@ function add_outage_results(m, p, d::Dict) tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps ) - r[string("mg", t, "_to_load_series")] = round.(value.(PVtoLoad), digits=3) + r[string("mg_", t, "_to_load_series")] = round.(value.(PVtoLoad), digits=3) end end @@ -139,11 +150,12 @@ function add_outage_results(m, p, d::Dict) if Bool(r[t * "_upgraded"]) r[string(t, "_mg_kw")] = round(value(m[:dvMGsize][t]), digits=4) else - r[string(t, "mg_kw")] = 0 + r[string(t, "_mg_kw")] = 0 end - r[string("mg_", t, "_fuel_used_per_outage_series")] = value.(m[:dvMGFuelUsed][t, :, :]).data + r[string("mg_", t, "_fuel_used_per_outage")] = value.(m[:dvMGFuelUsed][t, :, :]).data r[string("mg_", t, "_upgrade_cost")] = round(value(m[:dvMGTechUpgradeCost][t]), digits=2) + r["microgrid_upgrade_capital_cost"] += r[string("mg_", t, "_upgrade_cost")] if !isempty(p.s.storage.types.elec) GenToBatt = (m[:dvMGProductionToStorage][t, s, tz, ts] for @@ -153,16 +165,16 @@ function add_outage_results(m, p, d::Dict) else GenToBatt = [] end - r[string("mg", t, "_to_storage_series")] = round.(value.(GenToBatt), digits=3) + r[string("mg_", t, "_to_storage_series")] = round.(value.(GenToBatt), digits=3) GENtoCUR = (m[:dvMGCurtail][t, s, tz, ts] for 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) - r[string("mg", t, "_curtailed_series")] = round.(value.(GENtoCUR), digits=3) + r[string("mg_", t, "_curtailed_series")] = round.(value.(GENtoCUR), digits=3) GENtoLoad = ( - m[:dvMGRatedProduction][t, s, tz, ts] * p.production_factor[t, tz+ts] + m[:dvMGRatedProduction][t, s, tz, ts] * p.production_factor[t, tz+ts-1] * p.levelization_factor[t] - m[:dvMGCurtail][t, s, tz, ts] - m[:dvMGProductionToStorage][t, s, tz, ts] for @@ -170,8 +182,9 @@ function add_outage_results(m, p, d::Dict) tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps ) - r[string("mg", t, "_to_load_series")] = round.(value.(GENtoLoad), digits=3) + r[string("mg_", t, "_to_load_series")] = round.(value.(GENtoLoad), digits=3) end + r["generator_fuel_used_per_outage"] = sum(r[string("mg_", t, "_fuel_used_per_outage")] for t in p.techs.gen) end d["Outages"] = r end \ No newline at end of file diff --git a/src/results/results.jl b/src/results/results.jl index 4dbfc2462..4996be674 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -162,7 +162,7 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU ("ElectricUtility", "annual_energy_supplied_kwh"), ("ElectricUtility", "annual_emissions_tonnes_CO2"), ("ElectricUtility", "annual_emissions_tonnes_NOx"), - ("ElectricUtility", "annual_emissions_tonnes_NOx"), + ("ElectricUtility", "annual_emissions_tonnes_SO2"), ("ElectricUtility", "annual_emissions_tonnes_PM25"), ("ElectricUtility", "lifecycle_emissions_tonnes_CO2"), ("ElectricUtility", "lifecycle_emissions_tonnes_NOx"), @@ -224,7 +224,7 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU end end end - opt["Financial"]["lifecycle_om_costs_before_tax_bau"] = bau["Financial"]["lifecycle_om_costs_after_tax"] + opt["Financial"]["lifecycle_om_costs_before_tax_bau"] = bau["Financial"]["lifecycle_om_costs_before_tax"] opt["Financial"]["npv"] = round(opt["Financial"]["lcc_bau"] - opt["Financial"]["lcc"], digits=2) opt["ElectricLoad"]["bau_critical_load_met"] = bau_scenario.outage_outputs.bau_critical_load_met diff --git a/src/results/site.jl b/src/results/site.jl index b254cf36d..35f4093e2 100644 --- a/src/results/site.jl +++ b/src/results/site.jl @@ -60,6 +60,10 @@ calculated in combine_results function if BAU scenario is run: REopt performs load balances using average annual production values for technologies that include degradation. Therefore, all timeseries (`_series`) and `annual_` results should be interpretted as energy and emissions outputs averaged over the analysis period. +!!! note "Emissions outputs" + By default, REopt uses marginal emissions rates for grid-purchased electricity. Marginal emissions rates are most appropriate for reporting a change in emissions (avoided or increased) rather than emissions totals. + It is therefore recommended that emissions results from REopt (using default marginal emissions rates) be reported as the difference in emissions between the optimized and BAU case. + """ function add_site_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") diff --git a/test/scenarios/nogridcost_multiscenario.json b/test/scenarios/nogridcost_multiscenario.json index 5f0f61850..a18aeab7f 100644 --- a/test/scenarios/nogridcost_multiscenario.json +++ b/test/scenarios/nogridcost_multiscenario.json @@ -53,8 +53,9 @@ "critical_load_fraction": 0.1 }, "Generator": { + "max_kw": 0.0 }, "Financial": { - "value_of_lost_load_per_kwh": 0.0 + "value_of_lost_load_per_kwh": 1.0 } } diff --git a/test/scenarios/outage.json b/test/scenarios/outage.json index b35cadea8..354820e74 100644 --- a/test/scenarios/outage.json +++ b/test/scenarios/outage.json @@ -17,7 +17,9 @@ "existing_kw": 3580.54, "array_type": 0, "installed_cost_per_kw": 1600, - "om_cost_per_kw": 16 + "om_cost_per_kw": 16, + "federal_itc_fraction": 0.26, + "macrs_bonus_fraction": 1.0 }, "ElectricLoad": { "loads_kw": [4323.64, 4308.39, 4295.48, 4271.64, 4319.28, 4336.74, 4421.35, 4482.93, 4422.83, 4344.48, 4261.96, 4286.53, 4301.41, 4147.72, 4185.52, 4286.8, 4445.29, 4458.0, 4336.69, 4317.69, 4300.78, 4291.04, 4292.25, 4315.47, 4247.3, 4218.67, 4238.63, 4228.93, 4293.92, 4398.98, 4602.42, 4815.42, 4803.33, 4650.81, 4541.7, 4503.76, 4531.09, 4233.49, 4324.4, 4681.81, 4990.46, 4924.16, 4799.32, 4695.37, 4666.73, 4597.43, 4539.11, 4535.19, 4515.26, 4464.25, 4460.85, 4455.56, 4488.93, 4575.65, 4761.71, 4892.84, 4886.55, 4605.59, 4525.02, 4315.76, 4041.62, 3937.99, 4172.83, 4766.61, 5374.5, 5329.88, 5093.43, 4960.34, 4934.57, 4852.97, 4577.29, 4609.88, 4716.62, 4576.74, 4579.92, 4539.74, 4464.88, 4522.4, 4734.03, 5002.09, 4606.45, 4258.07, 3804.13, 3533.84, 3903.99, 3468.96, 4044.32, 4748.29, 5118.08, 4950.02, 4690.49, 4604.56, 4548.75, 4501.28, 4502.22, 4456.13, 4421.56, 4462.61, 4452.3, 4443.08, 4483.97, 4472.32, 4484.96, 4448.46, 3960.69, 3247.24, 2846.28, 2842.1, 2739.37, 3444.7, 3791.82, 4151.74, 4302.42, 4299.63, 4305.64, 4343.54, 4374.35, 4390.37, 4375.31, 4398.61, 4467.47, 4484.25, 4479.71, 4455.08, 4463.78, 4474.53, 4458.66, 4444.69, 4256.89, 3719.34, 2866.52, 3549.03, 3478.85, 3805.98, 3781.37, 3866.19, 4431.2, 4413.65, 4483.14, 4504.05, 4486.59, 4420.69, 4383.44, 4412.2, 4393.28, 4281.31, 4240.78, 4251.79, 4271.73, 4415.11, 4616.29, 4707.06, 4750.5, 3832.05, 3410.26, 3249.13, 3258.47, 3384.71, 3681.25, 4280.07, 4768.69, 4661.44, 4523.52, 4426.41, 4375.74, 4297.02, 4198.59, 4246.83, 4196.98, 4132.39, 4117.31, 4071.65, 4143.66, 4291.08, 4448.57, 4664.64, 4347.08, 4083.46, 3539.45, 3264.22, 3179.43, 3515.78, 3732.88, 4538.87, 4870.39, 4823.55, 4661.51, 4561.73, 4548.97, 4502.05, 4424.13, 4355.49, 4286.6, 4264.79, 4232.77, 4194.08, 4375.98, 4518.39, 4674.51, 4789.32, 4729.34, 4348.58, 3841.36, 3761.52, 3951.71, 4083.24, 4186.86, 4548.16, 4793.24, 4725.72, 4602.47, 4491.89, 4446.74, 4346.45, 4304.56, 4267.31, 4222.26, 4157.26, 4074.21, 4076.69, 4158.05, 4257.71, 4500.97, 4633.32, 4432.43, 3894.7, 3725.15, 3583.0, 3830.45, 4142.73, 4505.61, 4811.05, 4835.64, 4774.91, 4600.66, 4522.46, 4373.94, 4390.43, 4315.79, 4318.38, 4302.49, 4340.11, 4290.5, 4330.29, 4374.02, 4448.4, 4608.39, 4822.06, 5015.06, 5045.16, 5071.89, 5001.03, 4876.33, 4896.09, 4896.19, 4902.12, 4844.39, 4726.15, 4607.51, 4416.88, 4256.58, 4148.41, 4099.93, 4091.19, 4072.65, 4071.07, 4030.93, 4005.13, 4000.97, 4015.2, 4034.1, 4026.31, 3988.55, 4002.34, 4000.24, 3936.9, 3895.07, 3935.2, 3755.8, 3894.27, 4140.38, 4153.87, 4121.22, 4131.57, 4143.25, 4140.24, 4149.93, 4151.26, 4140.98, 4152.26, 4214.71, 4147.95, 4138.23, 4115.49, 4087.49, 4076.08, 3976.25, 3566.66, 3424.71, 3389.2, 3360.67, 3331.54, 3399.84, 3680.34, 3964.78, 4001.87, 4004.12, 4006.29, 4022.77, 4000.72, 3965.4, 3982.3, 3973.69, 3958.99, 3949.1, 3965.64, 4010.83, 4094.21, 4415.24, 4528.95, 4512.85, 4219.14, 4154.75, 4254.72, 4261.65, 4182.41, 4211.69, 4601.67, 4850.98, 4666.18, 4507.8, 4421.74, 4382.89, 4281.77, 4175.89, 4139.59, 4153.19, 4140.9, 4122.94, 4104.15, 4130.21, 4250.83, 4526.32, 4677.69, 4702.31, 4442.11, 4224.85, 4247.51, 4272.54, 4487.73, 4204.7, 4680.22, 4804.11, 4769.6, 4574.44, 4440.73, 4396.0, 4367.98, 4364.06, 4293.07, 4276.85, 4246.25, 4242.15, 4221.69, 4222.99, 4353.22, 4656.71, 4813.56, 4843.44, 4723.01, 4414.05, 4410.41, 4307.58, 4682.01, 4651.67, 5463.03, 4898.98, 4670.98, 4587.67, 4470.73, 4473.55, 4409.96, 4332.3, 4242.39, 4327.27, 4271.9, 4198.03, 4169.6, 4239.98, 4315.44, 4612.4, 4686.71, 4551.36, 4198.42, 4003.78, 3748.31, 3729.4, 3904.22, 4513.87, 4417.52, 4738.27, 4722.74, 4560.05, 4456.36, 4354.26, 4149.44, 4107.0, 4059.98, 4058.65, 4043.27, 4025.78, 4009.09, 4046.25, 4127.91, 4393.47, 4601.16, 4704.53, 4857.03, 5612.18, 5399.08, 4847.33, 5542.97, 4705.56, 4608.99, 4548.1, 4563.38, 4473.06, 4408.12, 4526.52, 4540.09, 4213.35, 4196.91, 4194.58, 4172.37, 4063.34, 4054.13, 4121.11, 4133.07, 4133.57, 4161.31, 3972.5, 3695.74, 3501.6, 3429.11, 3282.14, 3478.34, 3822.26, 3789.47, 3948.61, 3981.8, 3946.3, 3938.54, 3927.84, 3940.65, 3911.13, 3945.68, 3892.23, 3868.0, 3851.13, 3825.54, 3872.65, 3920.61, 4084.76, 4087.97, 3742.23, 3032.17, 2811.1, 2807.72, 2331.29, 2608.63, 2608.41, 3416.36, 3924.54, 3993.08, 3984.4, 4004.56, 4015.19, 3987.76, 3928.23, 3925.31, 3874.81, 3864.18, 3834.89, 3833.03, 3881.64, 3971.52, 4245.1, 4374.82, 3983.14, 3374.15, 3683.9, 3527.74, 3392.07, 2823.72, 4248.88, 4440.6, 4626.77, 4656.5, 4453.91, 4344.04, 4341.11, 4334.49, 4296.7, 4275.87, 4216.65, 4170.43, 4191.31, 4226.58, 4345.2, 4481.07, 4676.87, 4806.05, 4876.29, 4539.42, 4709.14, 4305.8, 4307.69, 4336.87, 4495.52, 4650.44, 4834.72, 4846.43, 4677.61, 4582.53, 4531.0, 4487.53, 4429.93, 4372.81, 4293.01, 4256.51, 4266.95, 4352.13, 4408.24, 4496.63, 4696.94, 4843.94, 4380.31, 4235.1, 4389.71, 4259.42, 4436.08, 4083.05, 4139.03, 4189.88, 4790.53, 4793.77, 4600.46, 4504.93, 4494.03, 4401.52, 4254.6, 4223.15, 4136.71, 4136.65, 4150.63, 4133.65, 4128.53, 4248.68, 4525.2, 4717.43, 4843.45, 4887.74, 4938.4, 5021.32, 5024.44, 4707.71, 4648.93, 4445.27, 4722.37, 4713.03, 4583.14, 4504.4, 4462.22, 4315.85, 4198.62, 4168.04, 4138.87, 4105.88, 4133.22, 4192.46, 4325.1, 4472.44, 4620.21, 4710.98, 4623.53, 4399.13, 4236.38, 3928.27, 3778.26, 3689.32, 4429.33, 4567.39, 4590.64, 4663.86, 4628.23, 4533.04, 4491.57, 4433.91, 4297.08, 4275.51, 4201.88, 4140.71, 4147.78, 4091.6, 4117.32, 4168.56, 4155.16, 4196.33, 3946.22, 3142.54, 3030.13, 2829.3, 2285.85, 2240.33, 2724.42, 3372.57, 4037.31, 4088.45, 4148.92, 4149.75, 4135.1, 4159.19, 4273.27, 4155.5, 4211.63, 4213.77, 4109.43, 4045.49, 4040.1, 4063.71, 4313.1, 4302.19, 3698.78, 3346.44, 3545.59, 3192.21, 3776.77, 3852.31, 3774.31, 3648.51, 3910.44, 4021.84, 4046.77, 4039.84, 3976.62, 4022.19, 4093.98, 4250.99, 4186.82, 4113.48, 4051.89, 4049.69, 4111.78, 4166.42, 4492.49, 4765.79, 4729.77, 4765.96, 4770.06, 4742.08, 4524.45, 4388.71, 4400.17, 4485.86, 4674.56, 4681.14, 4500.09, 4406.91, 4263.39, 4174.55, 4084.17, 4060.7, 4058.1, 4035.27, 4037.96, 4038.6, 4090.17, 4159.28, 4451.05, 4590.49, 4503.53, 4512.06, 4445.9, 4307.37, 4172.37, 4297.41, 4413.34, 4620.33, 4934.79, 4984.59, 4609.11, 4486.46, 4490.24, 4433.55, 4331.25, 4300.11, 4285.11, 4258.63, 4221.19, 4241.0, 4247.62, 4392.48, 4635.23, 4714.63, 4604.03, 4819.5, 4640.53, 4516.92, 4392.26, 4510.06, 4540.59, 4521.35, 4522.74, 4613.82, 4487.42, 4413.72, 4336.78, 4293.85, 4206.26, 4172.24, 4158.55, 4142.27, 4121.97, 4126.32, 4190.69, 4270.19, 4555.93, 4675.56, 4511.19, 4076.7, 3788.34, 3452.08, 3181.89, 2988.42, 3226.48, 3863.48, 4470.76, 4514.31, 4451.27, 4483.94, 4192.27, 4127.84, 4065.85, 4091.16, 4052.1, 4029.16, 4028.99, 4026.69, 4102.8, 4200.26, 4568.76, 4609.34, 4097.38, 3371.96, 2930.87, 2726.47, 2806.64, 2914.1, 3178.63, 3783.78, 4548.57, 4569.14, 4444.37, 4357.02, 4292.17, 4193.66, 4195.75, 4127.11, 4132.94, 4097.79, 4070.47, 4068.08, 4110.34, 4123.03, 4152.07, 4097.43, 3396.81, 2801.35, 2339.68, 2101.91, 2564.98, 3451.07, 3538.42, 3273.73, 3904.0, 3983.43, 3932.88, 3937.85, 3926.76, 3907.37, 3911.94, 3920.04, 3895.08, 3894.83, 3924.94, 3888.14, 3903.88, 3922.07, 3953.68, 3939.88, 3745.49, 3271.16, 2150.82, 1970.94, 1883.48, 1987.43, 2440.35, 3070.07, 3823.81, 3927.45, 3913.1, 3954.85, 3946.1, 3935.42, 3914.01, 3902.14, 3876.03, 3848.21, 3861.44, 3842.12, 3870.8, 3931.22, 4201.29, 4302.18, 3981.51, 3242.25, 3652.5, 3639.18, 3595.91, 3659.69, 3793.5, 4604.97, 4926.66, 4542.53, 4413.17, 4252.45, 4216.86, 4155.42, 4025.63, 4047.58, 4033.14, 4025.03, 4023.3, 4019.34, 4058.73, 4221.45, 4428.16, 4615.43, 4141.83, 3340.11, 2941.4, 2780.72, 2730.75, 2786.18, 2946.69, 3602.28, 4492.37, 4576.23, 4393.81, 4214.03, 4193.9, 4109.6, 4016.39, 3985.27, 3965.41, 3961.84, 3964.85, 3974.7, 4026.78, 4253.42, 4494.71, 4731.69, 4998.48, 4951.94, 4869.45, 4939.75, 5078.94, 4853.56, 4807.9, 4741.15, 4669.74, 4675.43, 4602.82, 4511.31, 4516.89, 4439.02, 4411.66, 4365.71, 4483.89, 4371.77, 4344.08, 4302.99, 4420.0, 4492.8, 4697.24, 4776.87, 4713.15, 4735.91, 4850.71, 4920.16, 4985.34, 4975.46, 5010.66, 4998.94, 5014.84, 4838.83, 4756.2, 4681.75, 4573.58, 4499.16, 4443.86, 4394.72, 4405.62, 4427.44, 4348.02, 4465.87, 4415.55, 4472.55, 4656.53, 4914.05, 4908.63, 4942.16, 4957.02, 4928.15, 4634.67, 4482.75, 4366.23, 4394.26, 4586.6, 4648.34, 4603.83, 4495.01, 4454.98, 4393.16, 4338.18, 4251.1, 4230.68, 4166.23, 4151.19, 4125.42, 4131.09, 4163.09, 4164.71, 4132.03, 3852.05, 3548.11, 3471.06, 3402.22, 3386.07, 3393.41, 3524.88, 3910.54, 4040.11, 4117.54, 4135.34, 4120.7, 4091.53, 4115.75, 4118.22, 4163.71, 4097.08, 4135.21, 4095.36, 4093.18, 4126.82, 4172.46, 4159.99, 4092.81, 3806.09, 3348.48, 3211.17, 3067.13, 2989.55, 2916.86, 2934.04, 3438.95, 3988.62, 4174.64, 4185.52, 4174.11, 4151.73, 4123.58, 4185.81, 4195.27, 4122.83, 4096.3, 4099.12, 4095.22, 4108.48, 4269.57, 4482.95, 4578.72, 4201.63, 3688.93, 3427.11, 3264.64, 3898.2, 3324.33, 4259.38, 4731.94, 4683.51, 4885.18, 4805.22, 4695.57, 4641.51, 4580.01, 4524.93, 4532.04, 4504.67, 4490.89, 4460.15, 4435.92, 4451.51, 4520.23, 4749.92, 4829.27, 4440.78, 3860.73, 3538.18, 4286.08, 3477.64, 3158.61, 3367.53, 4572.01, 4503.7, 4853.19, 4789.09, 4648.4, 4571.98, 4519.53, 4463.57, 4459.18, 4453.13, 4433.21, 4371.21, 4384.6, 4384.28, 4472.77, 4662.32, 4749.08, 4200.72, 3395.45, 3478.66, 3551.04, 3402.33, 4169.11, 3827.64, 4521.79, 4738.3, 4778.47, 4629.45, 4587.1, 4619.71, 4494.27, 4390.62, 4486.43, 4433.5, 4403.45, 4355.46, 4306.31, 4318.75, 4369.41, 4517.17, 4607.35, 4503.71, 4463.51, 4237.43, 4282.75, 4371.77, 4515.98, 4586.66, 4722.42, 4708.18, 4745.97, 4653.31, 4532.98, 4412.36, 4236.63, 4245.41, 4257.66, 4228.49, 4225.47, 4166.43, 4158.29, 4247.22, 4317.14, 4660.47, 4606.0, 3915.54, 3260.23, 2847.06, 2569.19, 2582.74, 2771.35, 3003.75, 3547.52, 4315.33, 4534.66, 4425.07, 4373.81, 4359.88, 4205.88, 4124.88, 4204.14, 4168.44, 4164.1, 4179.15, 4132.25, 4143.38, 4210.59, 4176.99, 3919.0, 3893.84, 3608.19, 2404.75, 3861.73, 4131.4, 4095.73, 3955.8, 3724.54, @@ -35,6 +37,7 @@ "ElectricStorage": { "can_grid_charge": false, "total_itc_fraction": 0.26, + "macrs_bonus_fraction": 1.0, "macrs_option_years": 5, "installed_cost_per_kw": 840.0, "installed_cost_per_kwh": 420.0, diff --git a/test/scenarios/pv.json b/test/scenarios/pv.json index 2910ac82c..538ee2786 100644 --- a/test/scenarios/pv.json +++ b/test/scenarios/pv.json @@ -4,7 +4,9 @@ "latitude": 34.5794343 }, "PV": { - "array_type": 0 + "array_type": 0, + "federal_itc_fraction": 0.26, + "macrs_bonus_fraction": 1.0 }, "ElectricLoad": { "doe_reference_name": "RetailStore", diff --git a/test/test_with_cplex.jl b/test/test_with_cplex.jl index ebd5dc0db..fd3c2f658 100644 --- a/test/test_with_cplex.jl +++ b/test/test_with_cplex.jl @@ -72,9 +72,9 @@ end @test results["expected_outage_cost"] ≈ 0 @test sum(results["unserved_load_per_outage"]) ≈ 0 @test value(m[:binMGTechUsed]["Generator"]) == 1 - @test value(m[:binMGTechUsed]["PV"]) == 0 + @test value(m[:binMGTechUsed]["PV"]) == 1 @test value(m[:binMGStorageUsed]) == 1 - @test results["Financial"]["lcc"] ≈ 7.3879557e7 atol=5e4 + @test results["Financial"]["lcc"] ≈ 7.19753998668e7 atol=5e4 #= Scenario with $0/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 6d318f9ea..1be9c8f0c 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -483,29 +483,32 @@ end end @testset "Minimize Unserved Load" begin + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) results = run_reopt(m, "./scenarios/outage.json") @test results["Outages"]["expected_outage_cost"] ≈ 0 - @test sum(results["Outages"]["unserved_load_per_outage_series"]) ≈ 0 - @test value(m[:binMGTechUsed]["Generator"]) == 1 - @test value(m[:binMGTechUsed]["PV"]) == 0 - @test value(m[:binMGStorageUsed]) == 1 - @test results["Financial"]["lcc"] ≈ 7.3879557e7 atol=5e4 - + @test sum(results["Outages"]["unserved_load_per_outage"]) ≈ 0 + @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 + @test value(m[:binMGTechUsed]["PV"]) ≈ 1 + @test value(m[:binMGStorageUsed]) ≈ 1 + @test results["Financial"]["lcc"] ≈ 7.19753998668e7 atol=5e4 + #= - Scenario with $0/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 + Scenario with $0.001/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 - should meet 168 kWh in each outage such that the total unserved load is 12 kWh =# m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) results = run_reopt(m, "./scenarios/nogridcost_minresilhours.json") - @test sum(results["Outages"]["unserved_load_per_outage_series"]) ≈ 12 + @test sum(results["Outages"]["unserved_load_per_outage"]) ≈ 12 # testing dvUnserved load, which would output 100 kWh for this scenario before output fix m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) results = run_reopt(m, "./scenarios/nogridcost_multiscenario.json") - @test sum(results["Outages"]["unserved_load_per_outage_series"]) ≈ 60 - + @test sum(results["Outages"]["unserved_load_per_outage"]) ≈ 60 + @test results["Outages"]["expected_outage_cost"] ≈ 485.43270 atol=1.0e-5 #avg duration (3h) * load per time step (10) * present worth factor (16.18109) + @test results["Outages"]["max_outage_cost_per_outage_duration"][1] ≈ 161.8109 atol=1.0e-5 + end @testset "Multiple Sites" begin @@ -641,7 +644,8 @@ end @testset "Wind" begin m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) - results = run_reopt(m, "./scenarios/wind.json") + d = JSON.parsefile("./scenarios/wind.json") + results = run_reopt(m, d) @test results["Wind"]["size_kw"] ≈ 3752 atol=0.1 @test results["Financial"]["lcc"] ≈ 8.591017e6 rtol=1e-5 #= @@ -658,6 +662,18 @@ end TODO: will these discrepancies be addressed once NMIL binaries are added? =# + + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + d["Site"]["land_acres"] = 60 # = 2 MW (with 0.03 acres/kW) + results = run_reopt(m, d) + @test results["Wind"]["size_kw"] == 2000.0 # Wind should be constrained by land_acres + + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + d["Wind"]["min_kw"] = 2001 # min_kw greater than land-constrained max should error + results = run_reopt(m, d) + @test "errors" ∈ keys(results["Messages"]) + @test length(results["Messages"]["errors"]) > 0 + end @testset "Multiple PVs" begin