Skip to content

Commit

Permalink
Merge pull request #147 from NREL/outages
Browse files Browse the repository at this point in the history
multiple outage modeling updates
  • Loading branch information
hdunham authored Feb 2, 2023
2 parents 60765b8 + 6e76f82 commit 95e74b0
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 84 deletions.
100 changes: 71 additions & 29 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## dev
## develop 01/19/2023
### 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
### Fixed
- PV results for all multi-node scenarios
- MPC objective definition w/o ElectricStorage
- fixed mulitple outages timestep off-by-one bug

## v0.24.0
### Changed
Expand Down Expand Up @@ -151,108 +160,137 @@ 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`
- add operating reserve requirement inputs, outputs, and constraints based on load and PV generation
- 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`
Expand All @@ -261,73 +299,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:
Expand All @@ -338,27 +379,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
Expand Down
10 changes: 5 additions & 5 deletions src/constraints/outage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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,
Expand Down Expand Up @@ -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],
Expand All @@ -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])
)
Expand Down
8 changes: 7 additions & 1 deletion src/core/electric_utility.jl
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,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),
Expand Down Expand Up @@ -207,6 +207,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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/financial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -120,7 +120,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],
Expand Down
Loading

0 comments on commit 95e74b0

Please sign in to comment.