From a6c74d09653eb216d101d3bd80dddcb8a35ec2b3 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 7 Jan 2025 08:52:14 -0700 Subject: [PATCH 01/66] initial commit --- CHANGELOG.md | 4 ++++ src/constraints/electric_utility_constraints.jl | 10 ++++++++-- src/constraints/storage_constraints.jl | 16 ++++++++++++++-- src/core/electric_tariff.jl | 2 +- src/core/energy_storage/electric_storage.jl | 4 ++++ src/core/reopt.jl | 2 ++ src/core/reopt_multinode.jl | 3 ++- src/mpc/model.jl | 2 ++ src/mpc/model_multinode.jl | 3 ++- src/mpc/structs.jl | 2 ++ src/results/electric_storage.jl | 9 +++++++-- src/results/pv.jl | 8 ++++---- src/results/thermal_storage.jl | 8 ++++---- 13 files changed, 56 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14896e890..6c74534b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## bess-export +### Added +- Added input option **can_export_to_grid** (defaults to _false_) to `ElectricStorage` and decision variable **dvStorageToGrid** +- ## Develop ### Added diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index c1842f578..fae322cae 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -129,8 +129,14 @@ function add_export_constraints(m, p; _n="") if typeof(binNEM) <: Real # no need for wholesale binary binWHL = 1 WHL_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] - for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) + # sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] + # for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) + sum(p.s.electric_tariff.export_rates[:WHL][ts] * + ( + sum(m[Symbol("dvStorageToGrid"*_n)][b, ts] for b in p.s.storage.types.elec) + ## TODO: index on metering type? + sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) + ) for ts in p.time_steps + ) ) else binWHL = @variable(m, binary = true) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index c46d4e775..2c8448d32 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -68,7 +68,7 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * ( sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + p.s.storage.attr[b].grid_charge_efficiency * m[Symbol("dvGridToStorage"*_n)][b, ts] - - m[Symbol("dvDischargeFromStorage"*_n)][b,ts] / p.s.storage.attr[b].discharge_efficiency + - ((m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts])/ p.s.storage.attr[b].discharge_efficiency) ) ) # Constraint (4g)-2: state-of-charge for electrical storage - no grid @@ -104,21 +104,33 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") @constraint(m, [ts in p.time_steps_with_grid], m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] + sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts] + + m[Symbol("dvStorageToGrid"*_n)][b, ts] ) + #Dispatch from electrical storage is no greater than power capacity + @constraint(m, [ts in p.time_steps_without_grid], + m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts]) + #Constraint (4l)-alt: Dispatch from electrical storage is no greater than power capacity (no grid connection) @constraint(m, [ts in p.time_steps_without_grid], m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) ) - # Remove grid-to-storage as an option if option to grid charge is turned off + #Constraint (4m)-1: Remove grid-to-storage as an option if option to grid charge is turned off if !(p.s.storage.attr[b].can_grid_charge) for ts in p.time_steps_with_grid fix(m[Symbol("dvGridToStorage"*_n)][b, ts], 0.0, force=true) end end + #Constraint (4m)-2: Force storage export to grid to zero if option to grid export is turned off + if !p.s.storage.attr[b].can_export_to_grid + for ts in p.time_steps + fix(m[Symbol("dvStorageToGrid"*_n)][b, ts], 0.0, force=true) + end + end + if p.s.storage.attr[b].minimum_avg_soc_fraction > 0 avg_soc = sum(m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) / (8760. / p.hours_per_time_step) diff --git a/src/core/electric_tariff.jl b/src/core/electric_tariff.jl index b18049a26..0a224f54d 100644 --- a/src/core/electric_tariff.jl +++ b/src/core/electric_tariff.jl @@ -395,7 +395,7 @@ Check length of e and upsample if length(e) != N """ function create_export_rate(e::AbstractArray{<:Real, 1}, N::Int, ts_per_hour::Int=1) Ne = length(e) - if Ne != Int(N/ts_per_hour) || Ne != N + if Ne != Int(N/ts_per_hour) && Ne != N throw(@error("Export rates do not have correct number of entries. Must be $(N) or $(Int(N/ts_per_hour)).")) end if Ne != N # upsample diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 771a84364..f01887022 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -167,6 +167,7 @@ end soc_min_applies_during_outages::Bool = false soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true + can_export_to_grid::Bool = false installed_cost_per_kw::Real = 910.0 installed_cost_per_kwh::Real = 455.0 replace_cost_per_kw::Real = 715.0 @@ -202,6 +203,7 @@ Base.@kwdef struct ElectricStorageDefaults soc_min_applies_during_outages::Bool = false soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true + can_export_to_grid::Bool = false installed_cost_per_kw::Real = 910.0 installed_cost_per_kwh::Real = 455.0 replace_cost_per_kw::Real = 715.0 @@ -243,6 +245,7 @@ struct ElectricStorage <: AbstractElectricStorage soc_min_applies_during_outages::Bool soc_init_fraction::Float64 can_grid_charge::Bool + can_export_to_grid::Bool installed_cost_per_kw::Real installed_cost_per_kwh::Real replace_cost_per_kw::Real @@ -336,6 +339,7 @@ struct ElectricStorage <: AbstractElectricStorage s.soc_min_applies_during_outages, s.soc_init_fraction, s.can_grid_charge, + s.can_export_to_grid, s.installed_cost_per_kw, s.installed_cost_per_kwh, replace_cost_per_kw, diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 662d249cd..22ce4ec4c 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -213,6 +213,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) @constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid], m[:dvProductionToStorage][b, t, ts] == 0) + @constraint(m, [ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, ts] == 0) # if there isn't a battery, then the battery can't export power to the grid elseif b in p.s.storage.types.hot @constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0) if "DomesticHotWater" in p.heating_loads_served_by_tes[b] @@ -616,6 +617,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) dvProductionToStorage[p.s.storage.types.all, union(p.techs.ghp,p.techs.all), p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] + dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] dvStorageEnergy[p.s.storage.types.all] >= 0 # Energy capacity of storage system b [kWh] diff --git a/src/core/reopt_multinode.jl b/src/core/reopt_multinode.jl index a41a80a35..c45887d7a 100644 --- a/src/core/reopt_multinode.jl +++ b/src/core/reopt_multinode.jl @@ -16,7 +16,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T} "dvStorageEnergy", ] dvs_idx_on_storagetypes_time_steps = String[ - "dvDischargeFromStorage" + "dvDischargeFromStorage", + "dvStorageToGrid" ] for p in ps _n = string("_", p.s.site.node) diff --git a/src/mpc/model.jl b/src/mpc/model.jl index d40a80da3..7a995a176 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -99,6 +99,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) @constraint(m, [ts in p.time_steps], m[:dvDischargeFromStorage][b, ts] == 0) if b in p.s.storage.types.elec @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) + @constraint(m, [ts in p.time_steps], m[:dvStorageToGrid][b, ts] == 0) end else add_general_storage_dispatch_constraints(m, p, b) @@ -230,6 +231,7 @@ function add_variables!(m::JuMP.AbstractModel, p::MPCInputs) dvCurtail[p.techs.all, p.time_steps] >= 0 # [kW] dvProductionToStorage[p.s.storage.types.all, p.techs.all, p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] + dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] diff --git a/src/mpc/model_multinode.jl b/src/mpc/model_multinode.jl index 68a3ef34a..245dd3582 100644 --- a/src/mpc/model_multinode.jl +++ b/src/mpc/model_multinode.jl @@ -152,7 +152,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{MPCInputs}) "dvRatedProduction", ] dvs_idx_on_storagetypes_time_steps = String[ - "dvDischargeFromStorage" + "dvDischargeFromStorage", + "dvStorageToGrid" ] for p in ps _n = string("_", p.s.node) diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index 340bb07c0..73a6ba022 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -226,6 +226,7 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true + can_export_to_grid::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 end ``` @@ -238,6 +239,7 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true + can_export_to_grid::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 max_kw::Float64 = size_kw max_kwh::Float64 = size_kwh diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index ef07eb48c..bd77baf11 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -3,8 +3,9 @@ `ElectricStorage` results keys: - `size_kw` Optimal inverter capacity - `size_kwh` Optimal storage capacity -- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year -- `storage_to_load_series_kw` Vector of power used to meet load over the first year +- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year +- `storage_to_load_series_kw` Vector of power used to meet load over an average year +- `storage_to_grid_series_kw` Vector of power exported to the grid over an average year - `initial_capital_cost` Upfront capital cost for storage and inverter # The following results are reported if storage degradation is modeled: - `state_of_health` @@ -44,9 +45,13 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: end r["residual_value"] = value(m[:residual_value]) end + + # report the exported electricity from the battery: + r["storage_to_grid_series_kw"] = round.(value.(m[Symbol("dvStorageToGrid"*_n)][b, ts] for ts in p.time_steps), digits = 3) else r["soc_series_fraction"] = [] r["storage_to_load_series_kw"] = [] + r["storage_to_grid_series_kw"] = [] end d[b] = r diff --git a/src/results/pv.jl b/src/results/pv.jl index 4c5774faa..b843ea957 100644 --- a/src/results/pv.jl +++ b/src/results/pv.jl @@ -6,10 +6,10 @@ - `year_one_energy_produced_kwh` Energy produced over the first year - `annual_energy_produced_kwh` Average annual energy produced when accounting for degradation - `lcoe_per_kwh` Levelized Cost of Energy produced by the PV system -- `electric_to_load_series_kw` Vector of power used to meet load over the first year -- `electric_to_storage_series_kw` Vector of power used to charge the battery over the first year -- `electric_to_grid_series_kw` Vector of power exported to the grid over the first year -- `electric_curtailed_series_kw` Vector of power curtailed over the first year +- `electric_to_load_series_kw` Vector of power used to meet load over an average year +- `electric_to_storage_series_kw` Vector of power used to charge the battery over an average year +- `electric_to_grid_series_kw` Vector of power exported to the grid over an average year +- `electric_curtailed_series_kw` Vector of power curtailed over an average year - `annual_energy_exported_kwh` Average annual energy exported to the grid - `production_factor_series` PV production factor in each time step, either provided by user or obtained from PVWatts diff --git a/src/results/thermal_storage.jl b/src/results/thermal_storage.jl index 48e9f607c..f6f0d7bfa 100644 --- a/src/results/thermal_storage.jl +++ b/src/results/thermal_storage.jl @@ -2,8 +2,8 @@ """ `HotThermalStorage` results keys: - `size_gal` Optimal TES capacity, by volume [gal] -- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year [-] -- `storage_to_load_series_mmbtu_per_hour` Vector of power used to meet load over the first year [MMBTU/hr] +- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year [-] +- `storage_to_load_series_mmbtu_per_hour` Vector of power used to meet load over an average year [MMBTU/hr] !!! note "'Series' and 'Annual' energy outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. @@ -84,8 +84,8 @@ end """ `ColdThermalStorage` results: - `size_gal` Optimal TES capacity, by volume [gal] -- `soc_series_fraction` Vector of normalized (0-1) state of charge values over the first year [-] -- `storage_to_load_series_ton` Vector of power used to meet load over the first year [ton] +- `soc_series_fraction` Vector of normalized (0-1) state of charge values over an average year [-] +- `storage_to_load_series_ton` Vector of power used to meet load over an average year [ton] """ function add_cold_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict, b::String; _n="") #= From 3cebd7e04ccd27a4e9ec0e1bf41130689119b107 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 10 Feb 2025 08:08:58 -0700 Subject: [PATCH 02/66] start adding inputs --- docs/src/developer/inputs.md | 2 +- src/core/energy_storage/electric_storage.jl | 28 +++++++++++++++++---- src/core/reopt_inputs.jl | 2 ++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/src/developer/inputs.md b/docs/src/developer/inputs.md index 9a380eafc..0c6332257 100644 --- a/docs/src/developer/inputs.md +++ b/docs/src/developer/inputs.md @@ -40,7 +40,7 @@ end The set maps are best explained with an example. The `techs_by_exportbin` map uses each technology'sattributes (eg. `PV`) to map each technology to which export bins that technology can access. The export bins include: 1. `:NEM` (Net Energy Metering) 2. `:WHL` (Wholesale) -3. `:EXC` (Excess, beyond NEM)) +3. `:EXC` (Excess, beyond NEM) 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( diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index f01887022..8c342e799 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -153,7 +153,7 @@ end """ -`ElectricStorage` is an optional optional REopt input with the following keys and default values: +`ElectricStorage` is an optional REopt input with the following keys and default values: ```julia min_kw::Real = 0.0 @@ -167,7 +167,10 @@ end soc_min_applies_during_outages::Bool = false soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true - can_export_to_grid::Bool = false + # can_export_to_grid::Bool = false + can_net_meter::Bool = false, + can_wholesale::Bool = false, + can_export_beyond_nem_limit = false, installed_cost_per_kw::Real = 910.0 installed_cost_per_kwh::Real = 455.0 replace_cost_per_kw::Real = 715.0 @@ -203,7 +206,10 @@ Base.@kwdef struct ElectricStorageDefaults soc_min_applies_during_outages::Bool = false soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true - can_export_to_grid::Bool = false + # can_export_to_grid::Bool = false + can_net_meter::Bool = false, + can_wholesale::Bool = false, + can_export_beyond_nem_limit = false, installed_cost_per_kw::Real = 910.0 installed_cost_per_kwh::Real = 455.0 replace_cost_per_kw::Real = 715.0 @@ -245,7 +251,10 @@ struct ElectricStorage <: AbstractElectricStorage soc_min_applies_during_outages::Bool soc_init_fraction::Float64 can_grid_charge::Bool - can_export_to_grid::Bool + # can_export_to_grid::Bool + can_net_meter::Bool + can_wholesale::Bool + can_export_beyond_nem_limit::Bool installed_cost_per_kw::Real installed_cost_per_kwh::Real replace_cost_per_kw::Real @@ -280,6 +289,13 @@ struct ElectricStorage <: AbstractElectricStorage @warn "Battery replacement costs (per_kwh) will not be considered because battery_replacement_year is greater than or equal to analysis_years." end + if off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) + @warn "Setting ElectricStorage can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." + can_net_meter = false + can_wholesale = false + can_export_beyond_nem_limit = false + end + # copy the replace_costs in case we need to change them replace_cost_per_kw = s.replace_cost_per_kw replace_cost_per_kwh = s.replace_cost_per_kwh @@ -339,7 +355,9 @@ struct ElectricStorage <: AbstractElectricStorage s.soc_min_applies_during_outages, s.soc_init_fraction, s.can_grid_charge, - s.can_export_to_grid, + s.can_net_meter, + s.can_wholesale, + s.can_export_beyond_nem_limit, s.installed_cost_per_kw, s.installed_cost_per_kwh, replace_cost_per_kw, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index af474ab1c..2aab5b9e4 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -471,6 +471,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) cooling_cf["GHP"] = ones(length(time_steps)) end + ## HERE . add setup_electric_storage_inputs? For techs_by_exportbin. + # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] From b211b4435f01b905e2bd939cc00281710ced490a Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 27 May 2025 16:42:09 -0600 Subject: [PATCH 03/66] rm can_grid_charge --- CHANGELOG.md | 5 +++-- src/core/energy_storage/electric_storage.jl | 2 +- src/mpc/structs.jl | 10 ++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d68d9413..ed1d8ec38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,9 @@ Classify the change according to the following categories: ## bess-export ### Added -- Added input option **can_export_to_grid** (defaults to _false_) to `ElectricStorage` and decision variable **dvStorageToGrid** -- +- Add **ElectricStorage** input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` +- TODO remove: was: add input option **can_export_to_grid** (defaults to _false_) to `ElectricStorage` and decision variable **dvStorageToGrid** + ## test-runners ### Added - Memory-clearing commands after each JuMP model instance in `runtests.jl` to avoid memory buildup which were slowing down Actions test job diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index d49d8fef6..b8fbd884c 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -359,7 +359,7 @@ struct ElectricStorage <: AbstractElectricStorage s.soc_min_fraction, s.soc_min_applies_during_outages, s.soc_init_fraction, - s.can_grid_charge, + # s.can_grid_charge, s.can_net_meter, s.can_wholesale, s.can_export_beyond_nem_limit, diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index 73a6ba022..c90bf87ce 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -226,7 +226,10 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - can_export_to_grid::Bool = false + # can_export_to_grid::Bool = false + can_net_meter::Bool = true + can_wholesale::Bool = false + can_export_beyond_nem_limit = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 end ``` @@ -239,7 +242,10 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - can_export_to_grid::Bool = false + # can_export_to_grid::Bool = false + can_net_meter::Bool = true + can_wholesale::Bool = false + can_export_beyond_nem_limit = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 max_kw::Float64 = size_kw max_kwh::Float64 = size_kwh From e065d2f1891ab70cb26aba8a6b4391d2fb9c7af9 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 28 May 2025 17:06:25 -0600 Subject: [PATCH 04/66] set up framework --- .../electric_utility_constraints.jl | 45 ++++++++++++------- src/core/bau_inputs.jl | 4 ++ src/core/reopt_inputs.jl | 38 +++++++++++++++- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index c731553f8..760dd7852 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -15,31 +15,36 @@ function add_export_constraints(m, p; _n="") WHL_benefit = 0 NEM_techs = String[t for t in p.techs.elec if :NEM in p.export_bins_by_tech[t]] WHL_techs = String[t for t in p.techs.elec if :WHL in p.export_bins_by_tech[t]] + NEM_storage = String[b for b in p.s.storage.types.elec if :NEM in p.export_bins_by_storage[b]] + WHL_storage = String[b for b in p.s.storage.types.elec if :WHL in p.export_bins_by_storage[b]] - if !isempty(NEM_techs) + if !isempty(NEM_techs + NEM_storage) # Constraint (9c): Net metering only -- can't sell more than you purchase # hours_per_time_step is cancelled on both sides, but used for unit consistency (convert power to energy) @constraint(m, p.hours_per_time_step * sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] - for t in NEM_techs, ts in p.time_steps) + for t in NEM_techs, ts in p.time_steps) # TODO: add storage <= p.hours_per_time_step * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) ) - if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(WHL_techs) + if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(WHL_techs + WHL_storage) # no need for binNEM nor binWHL binNEM = 1 + # TODO: Decide whether to ignore BESS size in this constraint? @constraint(m, sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw ) NEM_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) + # TODO: add storage ) if :EXC in p.s.electric_tariff.export_bins EXC_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps) + # TODO: add storage ) end else @@ -57,6 +62,7 @@ function add_export_constraints(m, p; _n="") # If choosing to take advantage of NEM, must have total capacity less than net_metering_limit_kw if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) + # TODO: Decide whether to ignore BESS size in these constraints? @constraint(m, binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.net_metering_limit_kw} ) @@ -88,23 +94,24 @@ function add_export_constraints(m, p; _n="") binNEM => {NEM_benefit >= p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) + # TODO: add storage } ) @constraint(m, !binNEM => {NEM_benefit >= 0}) @constraint(m,[ts in p.time_steps_with_grid, t in p.techs_by_exportbin[:NEM]], !binNEM => { m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] == 0 - } + } # TODO: add storage ) else @constraint(m, NEM_benefit >= p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] - for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) + for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) # TODO: add storage ) @constraint(m, NEM_benefit >= max_bene * binNEM) @constraint(m,[ts in p.time_steps_with_grid, t in p.techs_by_exportbin[:NEM]], m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] <= binNEM * sum(p.s.electric_load.loads_kw) - ) + ) # TODO: add storage end EXC_benefit = 0 @@ -115,7 +122,7 @@ function add_export_constraints(m, p; _n="") binNEM => {EXC_benefit >= p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps) - } + } # TODO: add storage ) @constraint(m, !binNEM => {EXC_benefit >= 0}) else @@ -123,14 +130,14 @@ function add_export_constraints(m, p; _n="") EXC_benefit >= p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps) - ) + ) # TODO: add storage @constraint(m, EXC_benefit >= max_bene * binNEM) end end end end - if !isempty(WHL_techs) + if !isempty(WHL_techs + WHL_storage) if typeof(binNEM) <: Real # no need for wholesale binary binWHL = 1 @@ -139,7 +146,7 @@ function add_export_constraints(m, p; _n="") # for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) sum(p.s.electric_tariff.export_rates[:WHL][ts] * ( - sum(m[Symbol("dvStorageToGrid"*_n)][b, ts] for b in p.s.storage.types.elec) + ## TODO: index on metering type? + sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + # TODO: add storage_by_exportbin sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) ) for ts in p.time_steps ) @@ -154,8 +161,14 @@ function add_export_constraints(m, p; _n="") if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) @constraint(m, binWHL => {WHL_benefit >= p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] - for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) + # sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] + # for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) + sum(p.s.electric_tariff.export_rates[:WHL][ts] * + ( + sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + + sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) + ) for ts in p.time_steps + ) } ) @constraint(m, !binWHL => {WHL_benefit >= 0}) @@ -164,7 +177,7 @@ function add_export_constraints(m, p; _n="") WHL_benefit >= p.pwf_e * p.hours_per_time_step * sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) - ) + ) # TODO: add storage @constraint(m, WHL_benefit >= max_bene * binWHL) end end @@ -195,7 +208,7 @@ function add_monthly_peak_constraint(m, p; _n="") sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t, ts] for t in p.techs.chp) - sum(sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types.elec) for t in p.techs.chp) - sum(sum(m[Symbol("dvProductionToGrid")][t,u,ts] for u in p.export_bins_by_tech[t]) for t in p.techs.chp) - + # TODO: add storage ) else @constraint(m, [mth in p.months, ts in p.s.electric_tariff.time_steps_monthly[mth]], @@ -282,7 +295,7 @@ function add_simultaneous_export_import_constraint(m, p; _n="") @constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps], !m[Symbol("binNoGridPurchases"*_n)][ts] => { sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= 0 - } + } # TODO: add storage ) else bigM_hourly_load = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal) @@ -292,7 +305,7 @@ function add_simultaneous_export_import_constraint(m, p; _n="") ) @constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps], sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= bigM_hourly_load * m[Symbol("binNoGridPurchases"*_n)][ts] - ) + ) # TODO: add storage end end diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 3464c39de..4beced61f 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -42,6 +42,8 @@ function BAUInputs(p::REoptInputs) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in p.s.electric_tariff.export_bins) export_bins_by_tech = Dict{String, Array{Symbol, 1}}() + storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in p.s.electric_tariff.export_bins) # Empty for BAU since cannot model "existing storage" + export_bins_by_storage = Dict{String, Array{Symbol, 1}}() # REoptInputs indexed on techs.segmented n_segs_by_tech = Dict{String, Int}() @@ -197,6 +199,8 @@ function BAUInputs(p::REoptInputs) p.ratchets, techs_by_exportbin, export_bins_by_tech, + storage_by_exportbin, + export_bins_by_storage, n_segs_by_tech, seg_min_size, seg_max_size, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 81395ac64..df5d71a28 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -35,6 +35,8 @@ struct REoptInputs <: AbstractInputs ratchets::UnitRange techs_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :CUR] export_bins_by_tech::Dict + storage_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL] + export_bins_by_storage::Dict n_segs_by_tech::Dict{String, Int} seg_min_size::Dict{String, Dict{Int, <:Real}} seg_max_size::Dict{String, Dict{Int, <:Real}} @@ -100,6 +102,8 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs ratchets::UnitRange techs_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :CUR] export_bins_by_tech::Dict + storage_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL] + export_bins_by_storage::Dict n_segs_by_tech::Dict{String, Int} seg_min_size::Dict{String, Dict{Int, Real}} seg_max_size::Dict{String, Dict{Int, Real}} @@ -170,7 +174,8 @@ function REoptInputs(s::AbstractScenario) hours_per_time_step = 1 / s.settings.time_steps_per_hour techs, pv_to_location, maxsize_pv_locations, pvlocations, production_factor, max_sizes, min_sizes, existing_sizes, cap_cost_slope, om_cost_per_kw, n_segs_by_tech, - seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, + seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, + storage_by_exportbin, export_bins_by_storage, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, heating_cop, cooling_cop, heating_cf, cooling_cf, avoided_capex_by_ashp_present_value, @@ -290,6 +295,8 @@ function REoptInputs(s::AbstractScenario) 1:length(s.electric_tariff.tou_demand_ratchet_time_steps), # ratchets techs_by_exportbin, export_bins_by_tech, + storage_by_exportbin, + export_bins_by_storage, n_segs_by_tech, seg_min_size, seg_max_size, @@ -374,6 +381,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) export_bins_by_tech = Dict{String, Array{Symbol, 1}}() + storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in p.s.electric_tariff.export_bins) + export_bin_by_storage = Dict{String, Array{Symbol, 1}}() # REoptInputs indexed on techs.segmented n_segs_by_tech = Dict{String, Int}() @@ -484,6 +493,15 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] end + + if !isempty(s.storage.types.elec) + for b in s.storage.types.elec + fillin_storage_by_exportbin(storage_by_exportbin, b, "Temp") # TODO: figure out if a name should be used. + export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] + println(storage_by_exportbin) + println(export_bins_by_storage) + end + end if s.settings.off_grid_flag setup_operating_reserve_fraction(s, techs_operating_reserve_req_fraction) @@ -491,7 +509,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) return techs, pv_to_location, maxsize_pv_locations, pvlocations, production_factor, max_sizes, min_sizes, existing_sizes, cap_cost_slope, om_cost_per_kw, n_segs_by_tech, - seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, + seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, + storage_by_exportbin, export_bins_by_storage, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, heating_cop, cooling_cop, heating_cf, cooling_cf, avoided_capex_by_ashp_present_value, @@ -1210,6 +1229,21 @@ function fillin_techs_by_exportbin(techs_by_exportbin::Dict, tech::AbstractTech, return nothing end +function fillin_storage_by_exportbin(storage_by_exportbin::Dict, stor::AbstractStorage, storage_name::String) + #TODO: assess what stor and storage_name will/should be. + if stor.can_net_meter && :NEM in keys(storage_by_exportbin) + push!(storage_by_exportbin[:NEM], storage_name) + if stor.can_export_beyond_nem_limit && :EXC in keys(storage_by_exportbin) + push!(storage_by_exportbin[:EXC], storage_name) + end + end + + if stor.can_wholesale && :WHL in keys(storage_by_exportbin) + push!(storage_by_exportbin[:WHL], storage_name) + end + return nothing +end + function setup_ghp_inputs(s::AbstractScenario, time_steps, time_steps_without_grid) # GHP parameters for REopt model num = length(s.ghp_option_list) From 52f036b908716e807e9c46d7a7345a7149125d6c Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 30 May 2025 10:29:53 -0600 Subject: [PATCH 05/66] update electric_utility_constraints --- .../electric_utility_constraints.jl | 119 +++++++++++------- src/core/reopt.jl | 9 +- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 760dd7852..d2d91a676 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -18,17 +18,19 @@ function add_export_constraints(m, p; _n="") NEM_storage = String[b for b in p.s.storage.types.elec if :NEM in p.export_bins_by_storage[b]] WHL_storage = String[b for b in p.s.storage.types.elec if :WHL in p.export_bins_by_storage[b]] - if !isempty(NEM_techs + NEM_storage) + if !isempty(vcat(NEM_techs, NEM_storage)) # Constraint (9c): Net metering only -- can't sell more than you purchase # hours_per_time_step is cancelled on both sides, but used for unit consistency (convert power to energy) @constraint(m, - p.hours_per_time_step * sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] - for t in NEM_techs, ts in p.time_steps) # TODO: add storage + p.hours_per_time_step * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in NEM_techs, ts in p.time_steps) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in NEM_storage, ts in p.time_steps) + ) <= p.hours_per_time_step * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) ) - if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(WHL_techs + WHL_storage) + if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(vcat(WHL_techs, WHL_storage)) # no need for binNEM nor binWHL binNEM = 1 # TODO: Decide whether to ignore BESS size in this constraint? @@ -36,15 +38,17 @@ function add_export_constraints(m, p; _n="") sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw ) NEM_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] - for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) - # TODO: add storage + sum(p.s.electric_tariff.export_rates[:NEM][ts] * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in p.techs_by_exportbin[:NEM]) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in p.storage_by_exportbin[:NEM]) + ) for ts in p.time_steps) ) if :EXC in p.s.electric_tariff.export_bins EXC_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] - for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps) - # TODO: add storage + sum(p.s.electric_tariff.export_rates[:EXC][ts] * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] for t in p.techs_by_exportbin[:EXC]) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :EXC, ts] for b in p.storage_by_exportbin[:EXC]) + ) for ts in p.time_steps) ) end else @@ -92,26 +96,38 @@ function add_export_constraints(m, p; _n="") if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) @constraint(m, binNEM => {NEM_benefit >= p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] - for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) - # TODO: add storage + sum(p.s.electric_tariff.export_rates[:NEM][ts] * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in p.techs_by_exportbin[:NEM]) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in p.storage_by_exportbin[:NEM]) + ) for ts in p.time_steps + ) } ) @constraint(m, !binNEM => {NEM_benefit >= 0}) @constraint(m,[ts in p.time_steps_with_grid, t in p.techs_by_exportbin[:NEM]], !binNEM => { m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] == 0 - } # TODO: add storage + } + ) + @constraint(m,[ts in p.time_steps_with_grid, b in p.storage_by_exportbin[:NEM]], + !binNEM => { m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] == 0 + } ) else @constraint(m, NEM_benefit >= p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:NEM][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] - for t in p.techs_by_exportbin[:NEM]) for ts in p.time_steps) # TODO: add storage + sum(p.s.electric_tariff.export_rates[:NEM][ts] * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in p.techs_by_exportbin[:NEM]) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in p.storage_by_exportbin[:NEM]) + ) for ts in p.time_steps + ) ) @constraint(m, NEM_benefit >= max_bene * binNEM) @constraint(m,[ts in p.time_steps_with_grid, t in p.techs_by_exportbin[:NEM]], m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] <= binNEM * sum(p.s.electric_load.loads_kw) - ) # TODO: add storage + ) + @constraint(m,[ts in p.time_steps_with_grid, b in p.storage_by_exportbin[:NEM]], + m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] <= binNEM * sum(p.s.electric_load.loads_kw) + ) end EXC_benefit = 0 @@ -120,36 +136,41 @@ function add_export_constraints(m, p; _n="") if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) @constraint(m, binNEM => {EXC_benefit >= p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] - for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps) - } # TODO: add storage + sum( p.s.electric_tariff.export_rates[:EXC][ts] * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] for t in p.techs_by_exportbin[:EXC]) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :EXC, ts] for b in p.storage_by_exportbin[:EXC]) + ) for ts in p.time_steps + ) + } ) + + @constraint(m, !binNEM => {EXC_benefit >= 0}) else @constraint(m, EXC_benefit >= p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:EXC][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] - for t in p.techs_by_exportbin[:EXC]) for ts in p.time_steps) - ) # TODO: add storage + sum( p.s.electric_tariff.export_rates[:EXC][ts] * ( + sum( m[Symbol("dvProductionToGrid"*_n)][t, :EXC, ts] for t in p.techs_by_exportbin[:EXC]) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :EXC, ts] for b in p.storage_by_exportbin[:EXC]) + ) for ts in p.time_steps + ) + ) @constraint(m, EXC_benefit >= max_bene * binNEM) end end end end - if !isempty(WHL_techs + WHL_storage) + if !isempty(vcat(WHL_techs, WHL_storage)) if typeof(binNEM) <: Real # no need for wholesale binary binWHL = 1 WHL_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * - # sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] - # for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) - sum(p.s.electric_tariff.export_rates[:WHL][ts] * - ( - sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + # TODO: add storage_by_exportbin - sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) - ) for ts in p.time_steps - ) + sum( p.s.electric_tariff.export_rates[:WHL][ts] * ( + sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + ) for ts in p.time_steps + ) ) else binWHL = @variable(m, binary = true) @@ -161,13 +182,10 @@ function add_export_constraints(m, p; _n="") if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) @constraint(m, binWHL => {WHL_benefit >= p.pwf_e * p.hours_per_time_step * - # sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] - # for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) - sum(p.s.electric_tariff.export_rates[:WHL][ts] * - ( - sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + - sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) - ) for ts in p.time_steps + sum(p.s.electric_tariff.export_rates[:WHL][ts] * ( + sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + ) for ts in p.time_steps ) } ) @@ -175,9 +193,12 @@ function add_export_constraints(m, p; _n="") else @constraint(m, WHL_benefit >= p.pwf_e * p.hours_per_time_step * - sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] - for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps) - ) # TODO: add storage + sum(p.s.electric_tariff.export_rates[:WHL][ts] * ( + sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL]) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] for b in p.storage_by_exportbin[:WHL]) + ) for ts in p.time_steps + ) + ) @constraint(m, WHL_benefit >= max_bene * binWHL) end end @@ -208,7 +229,6 @@ function add_monthly_peak_constraint(m, p; _n="") sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t, ts] for t in p.techs.chp) - sum(sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types.elec) for t in p.techs.chp) - sum(sum(m[Symbol("dvProductionToGrid")][t,u,ts] for u in p.export_bins_by_tech[t]) for t in p.techs.chp) - # TODO: add storage ) else @constraint(m, [mth in p.months, ts in p.s.electric_tariff.time_steps_monthly[mth]], @@ -294,8 +314,9 @@ function add_simultaneous_export_import_constraint(m, p; _n="") ) @constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps], !m[Symbol("binNoGridPurchases"*_n)][ts] => { - sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= 0 - } # TODO: add storage + sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) <= 0 + } ) else bigM_hourly_load = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal) @@ -304,8 +325,10 @@ function add_simultaneous_export_import_constraint(m, p; _n="") sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) <= bigM_hourly_load*(1-m[Symbol("binNoGridPurchases"*_n)][ts]) ) @constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps], - sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= bigM_hourly_load * m[Symbol("binNoGridPurchases"*_n)][ts] - ) # TODO: add storage + sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + <= bigM_hourly_load * m[Symbol("binNoGridPurchases"*_n)][ts] + ) end end @@ -395,7 +418,7 @@ end function add_elec_utility_expressions(m, p; _n="") if !isempty(p.s.electric_tariff.export_bins) && !isempty(p.techs.all) - # NOTE: levelization_factor is baked into dvProductionToGrid + # NOTE: levelization_factor is baked into dvProductionToGrid and dvStorageToGrid m[Symbol("TotalExportBenefit"*_n)] = m[Symbol("NEM_benefit"*_n)] + m[Symbol("WHL_benefit"*_n)] + m[Symbol("EXC_benefit"*_n)] else diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 6003f8018..81e65b3d5 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -199,6 +199,9 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) if !isempty(p.s.electric_tariff.export_bins) for t in p.techs.elec, u in p.export_bins_by_tech[t] fix(m[:dvProductionToGrid][t, u, ts], 0.0, force=true) + end + for b in p.s.storage.types.elec, u in p.export_bins_by_tech[t] + fix(m[:dvStorageToGrid][b, u, ts], 0.0, force=true) end end end @@ -213,7 +216,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) @constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid], m[:dvProductionToStorage][b, t, ts] == 0) - @constraint(m, [ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, ts] == 0) # if there isn't a battery, then the battery can't export power to the grid + # TODO: add "u" index below; and decide if this constraint is needed. + # @constraint(m, [ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, ts] == 0) # if there isn't a battery, then the battery can't export power to the grid elseif b in p.s.storage.types.hot @constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0) if "DomesticHotWater" in p.heating_loads_served_by_tes[b] @@ -623,7 +627,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) dvProductionToStorage[p.s.storage.types.all, union(p.techs.ghp,p.techs.all), p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] - dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid + # TODO determine if needed here: dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] dvStorageEnergy[p.s.storage.types.all] >= 0 # Energy capacity of storage system b [kWh] @@ -646,6 +650,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) if !isempty(p.s.electric_tariff.export_bins) @variable(m, dvProductionToGrid[p.techs.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) + @variable(m, dvStorageToGrid[p.s.storage.types.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) end if !(p.s.electric_utility.allow_simultaneous_export_import) & !isempty(p.s.electric_tariff.export_bins) From 47bfc7967a9469ed6c2b739773f3d2da31fbfa5d Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 30 May 2025 13:54:52 -0600 Subject: [PATCH 06/66] make discharge from storage total discharge --- .../electric_utility_constraints.jl | 1 + src/constraints/load_balance.jl | 1 + src/constraints/storage_constraints.jl | 24 +++++++++---------- src/mpc/model.jl | 4 ++++ 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index d2d91a676..29fca396c 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -319,6 +319,7 @@ function add_simultaneous_export_import_constraint(m, p; _n="") } ) else + #TODO: consider adding max battery power here like in https://github.com/NREL/REopt.jl/pull/447/files bigM_hourly_load = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal) @constraint(m, NoGridPurchasesBinary[ts in p.time_steps], sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) + diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index bdb93fa24..7e507fba0 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -27,6 +27,7 @@ function add_elec_load_balance_constraints(m, p; _n="") sum(sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for u in p.export_bins_by_tech[t]) + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index a73d906a5..a7d0ceb43 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -33,7 +33,6 @@ function add_storage_size_constraints(m, p, b; _n="") ) end - end @@ -74,12 +73,21 @@ end function add_elec_storage_dispatch_constraints(m, p, b; _n="") - # Constraint (4g)-1: state-of-charge for electrical storage - with grid + # Constraint (4d): Storage export must be less than total storage dispatch + if !isempty(p.export_bins_by_storage[b]) # TODO: Is this if statement needed? + @constraint(m, [b in p.s.storage.attr[b], ts in p.time_steps_with_grid], + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for u in p.export_bins_by_storage[b]) + <= + m[Symbol("dvDischargeFromStorage"*_n)][b, ts] # TODO: should this be divided by p.s.storage.attr[b].discharge_efficiency ? + ) + end + + # Constraint (4g)-1: state-of-charge for electrical storage - with grid @constraint(m, [ts in p.time_steps_with_grid], m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * ( sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + p.s.storage.attr[b].grid_charge_efficiency * m[Symbol("dvGridToStorage"*_n)][b, ts] - - ((m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts])/ p.s.storage.attr[b].discharge_efficiency) + - (m[Symbol("dvDischargeFromStorage"*_n)][b,ts] / p.s.storage.attr[b].discharge_efficiency) ) ) # Constraint (4g)-2: state-of-charge for electrical storage - no grid @@ -115,12 +123,11 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") @constraint(m, [ts in p.time_steps_with_grid], m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] + sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts] - + m[Symbol("dvStorageToGrid"*_n)][b, ts] ) #Dispatch from electrical storage is no greater than power capacity @constraint(m, [ts in p.time_steps_without_grid], - m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts]) + m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts]) #Constraint (4l)-alt: Dispatch from electrical storage is no greater than power capacity (no grid connection) @constraint(m, [ts in p.time_steps_without_grid], @@ -135,13 +142,6 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") end end - #Constraint (4m)-2: Force storage export to grid to zero if option to grid export is turned off - if !p.s.storage.attr[b].can_export_to_grid - for ts in p.time_steps - fix(m[Symbol("dvStorageToGrid"*_n)][b, ts], 0.0, force=true) - end - end - if p.s.storage.attr[b].minimum_avg_soc_fraction > 0 avg_soc = sum(m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) / (8760. / p.hours_per_time_step) diff --git a/src/mpc/model.jl b/src/mpc/model.jl index 7a995a176..1b0de5995 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -89,6 +89,10 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) for t in p.techs.elec, u in p.export_bins_by_tech[t] fix(m[:dvProductionToGrid][t, u, ts], 0.0, force=true) end + + for b in p.s.storage.types.elec, u in p.export_bins_by_tech[t] + fix(m[:dvStorageToGrid][b, u, ts], 0.0, force=true) + end end for b in p.s.storage.types.all From 8d9be522c1fe3f6aafbde97d9498b876f8ef2737 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 30 May 2025 14:54:01 -0600 Subject: [PATCH 07/66] update results --- src/core/reopt.jl | 6 +++--- src/mpc/model.jl | 5 +++-- src/mpc/model_multinode.jl | 1 + src/results/electric_storage.jl | 19 +++++++++++++------ 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 81e65b3d5..ab424404e 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -216,8 +216,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) @constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid], m[:dvProductionToStorage][b, t, ts] == 0) - # TODO: add "u" index below; and decide if this constraint is needed. - # @constraint(m, [ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, ts] == 0) # if there isn't a battery, then the battery can't export power to the grid + # TODO: is the below needed? + @constraint(m, [u in p.export_bins_by_storage[b], ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, u, ts] == 0) elseif b in p.s.storage.types.hot @constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0) if "DomesticHotWater" in p.heating_loads_served_by_tes[b] @@ -627,7 +627,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) dvProductionToStorage[p.s.storage.types.all, union(p.techs.ghp,p.techs.all), p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] - # TODO determine if needed here: dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid + # TODO determine if needed here: dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # export of energy from storage to the grid dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] dvStorageEnergy[p.s.storage.types.all] >= 0 # Energy capacity of storage system b [kWh] diff --git a/src/mpc/model.jl b/src/mpc/model.jl index 1b0de5995..e658e2737 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -103,7 +103,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) @constraint(m, [ts in p.time_steps], m[:dvDischargeFromStorage][b, ts] == 0) if b in p.s.storage.types.elec @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) - @constraint(m, [ts in p.time_steps], m[:dvStorageToGrid][b, ts] == 0) + @constraint(m, [u in p.export_bins_by_storage[b], ts in p.time_steps], m[:dvStorageToGrid][b, u, ts] == 0) end else add_general_storage_dispatch_constraints(m, p, b) @@ -235,7 +235,7 @@ function add_variables!(m::JuMP.AbstractModel, p::MPCInputs) dvCurtail[p.techs.all, p.time_steps] >= 0 # [kW] dvProductionToStorage[p.s.storage.types.all, p.techs.all, p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] - dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid + # dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] @@ -249,6 +249,7 @@ function add_variables!(m::JuMP.AbstractModel, p::MPCInputs) if !isempty(p.s.electric_tariff.export_bins) @variable(m, dvProductionToGrid[p.techs.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) + @variable(m, dvStorageToGrid[p.storage.types.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) end m[:dvSize] = p.existing_sizes diff --git a/src/mpc/model_multinode.jl b/src/mpc/model_multinode.jl index 245dd3582..3748dc538 100644 --- a/src/mpc/model_multinode.jl +++ b/src/mpc/model_multinode.jl @@ -66,6 +66,7 @@ function build_mpc!(m::JuMP.AbstractModel, ps::AbstractVector{MPCInputs}) @constraint(m, [ts in p.time_steps], m[Symbol("dvDischargeFromStorage"*_n)][b, ts] == 0) if b in p.s.storage.types.elec @constraint(m, [ts in p.time_steps], m[Symbol("dvGridToStorage"*_n)][b, ts] == 0) + # TODO: add constraint on storage to grid here end else add_general_storage_dispatch_constraints(m, p, b; _n=_n) diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index bd77baf11..3e2ec8667 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -29,8 +29,18 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: soc = (m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) r["soc_series_fraction"] = round.(value.(soc) ./ r["size_kwh"], digits=3) - discharge = (m[Symbol("dvDischargeFromStorage"*_n)][b, ts] for ts in p.time_steps) - r["storage_to_load_series_kw"] = round.(value.(discharge), digits=3) + if !isempty(p.s.electric_tariff.export_bins) + StorageToGrid = @expression(m, [ts in p.time_steps], + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for u in p.export_bins_by_storage[b])) + else + StorageToGrid = repeat([0], length(p.time_steps)) + end + + StorageToLoad = ( m[Symbol("dvDischargeFromStorage"*_n)][b, ts] + - StorageToGrid[ts] for ts in p.time_steps + ) + r["storage_to_grid_series_kw"] = round.(value.(StorageToGrid), digits=3) + r["storage_to_load_series_kw"] = round.(value.(StorageToLoad), digits=3) r["initial_capital_cost"] = r["size_kwh"] * p.s.storage.attr[b].installed_cost_per_kwh + r["size_kw"] * p.s.storage.attr[b].installed_cost_per_kw @@ -44,10 +54,7 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: )) end r["residual_value"] = value(m[:residual_value]) - end - - # report the exported electricity from the battery: - r["storage_to_grid_series_kw"] = round.(value.(m[Symbol("dvStorageToGrid"*_n)][b, ts] for ts in p.time_steps), digits = 3) + end else r["soc_series_fraction"] = [] r["storage_to_load_series_kw"] = [] From b897e47e580655266a8518959f5c7704f12f8bb2 Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 2 Jun 2025 10:21:27 -0600 Subject: [PATCH 08/66] fix reopt_inputs --- src/core/energy_storage/electric_storage.jl | 26 +++++++++---------- src/core/reopt_inputs.jl | 28 ++++++++++----------- src/core/scenario.jl | 1 - 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index b8fbd884c..7bb54026b 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -170,9 +170,9 @@ end soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true # can_export_to_grid::Bool = false - can_net_meter::Bool = false, - can_wholesale::Bool = false, - can_export_beyond_nem_limit = false, + can_net_meter::Bool = false + can_wholesale::Bool = false + can_export_beyond_nem_limit = false installed_cost_per_kw::Real = 910.0 installed_cost_per_kwh::Real = 455.0 replace_cost_per_kw::Real = 715.0 @@ -210,9 +210,9 @@ Base.@kwdef struct ElectricStorageDefaults soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true # can_export_to_grid::Bool = false - can_net_meter::Bool = false, - can_wholesale::Bool = false, - can_export_beyond_nem_limit = false, + can_net_meter::Bool = false + can_wholesale::Bool = false + can_export_beyond_nem_limit = false installed_cost_per_kw::Real = 910.0 installed_cost_per_kwh::Real = 455.0 replace_cost_per_kw::Real = 715.0 @@ -294,12 +294,12 @@ struct ElectricStorage <: AbstractElectricStorage @warn "Battery replacement costs (per_kwh) will not be considered because battery_replacement_year is greater than or equal to analysis_years." end - if off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) - @warn "Setting ElectricStorage can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." - can_net_meter = false - can_wholesale = false - can_export_beyond_nem_limit = false - end + # if s.off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) + # @warn "Setting ElectricStorage can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." + # can_net_meter = false + # can_wholesale = false + # can_export_beyond_nem_limit = false + # end # copy the replace_costs in case we need to change them replace_cost_per_kw = s.replace_cost_per_kw @@ -359,7 +359,7 @@ struct ElectricStorage <: AbstractElectricStorage s.soc_min_fraction, s.soc_min_applies_during_outages, s.soc_init_fraction, - # s.can_grid_charge, + s.can_grid_charge, s.can_net_meter, s.can_wholesale, s.can_export_beyond_nem_limit, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index df5d71a28..007251107 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -379,10 +379,11 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) pbi_benefit_per_kwh = Dict{String, Any}() # export related inputs + @info "s.electric_tariff.export_bins: $(s.electric_tariff.export_bins)" techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) export_bins_by_tech = Dict{String, Array{Symbol, 1}}() - storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in p.s.electric_tariff.export_bins) - export_bin_by_storage = Dict{String, Array{Symbol, 1}}() + storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) # TODO remove CUR? + export_bins_by_storage = Dict{String, Array{Symbol, 1}}() # REoptInputs indexed on techs.segmented n_segs_by_tech = Dict{String, Int}() @@ -493,13 +494,13 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] end - + if !isempty(s.storage.types.elec) for b in s.storage.types.elec - fillin_storage_by_exportbin(storage_by_exportbin, b, "Temp") # TODO: figure out if a name should be used. + fillin_storage_by_exportbin(s, storage_by_exportbin, b) export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] - println(storage_by_exportbin) - println(export_bins_by_storage) + @info "export_bins_by_storage: $(export_bins_by_storage)" + @info "storage_by_exportbin: $(storage_by_exportbin)" end end @@ -1229,17 +1230,16 @@ function fillin_techs_by_exportbin(techs_by_exportbin::Dict, tech::AbstractTech, return nothing end -function fillin_storage_by_exportbin(storage_by_exportbin::Dict, stor::AbstractStorage, storage_name::String) - #TODO: assess what stor and storage_name will/should be. - if stor.can_net_meter && :NEM in keys(storage_by_exportbin) - push!(storage_by_exportbin[:NEM], storage_name) - if stor.can_export_beyond_nem_limit && :EXC in keys(storage_by_exportbin) - push!(storage_by_exportbin[:EXC], storage_name) +function fillin_storage_by_exportbin(s::AbstractScenario, storage_by_exportbin::Dict, b::String) + if s.storage.attr[b].can_net_meter && :NEM in keys(storage_by_exportbin) + push!(storage_by_exportbin[:NEM], b) + if s.storage.attr[b].can_export_beyond_nem_limit && :EXC in keys(storage_by_exportbin) + push!(storage_by_exportbin[:EXC], b) end end - if stor.can_wholesale && :WHL in keys(storage_by_exportbin) - push!(storage_by_exportbin[:WHL], storage_name) + if s.storage.attr[b].can_wholesale && :WHL in keys(storage_by_exportbin) + push!(storage_by_exportbin[:WHL], b) end return nothing end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 77c8cf26b..7fc2ce32e 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -42,7 +42,6 @@ A Scenario struct can contain the following keys: - [ElectricStorage](@ref) (optional) - [HotThermalStorage](@ref) (optional) - [ColdThermalStorage](@ref) (optional) -- [ElectricStorage](@ref) (optional) - [ElectricUtility](@ref) (optional) - [Generator](@ref) (optional) - [HeatingLoad](@ref) (optional) From 5ac4c843cb90d97e8c6956b949f99266210a3e7b Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 2 Jun 2025 13:53:02 -0600 Subject: [PATCH 09/66] fix indexing --- src/core/reopt.jl | 2 +- src/mpc/model.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 4fabe2dbc..bd1f9a707 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -200,7 +200,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) for t in p.techs.elec, u in p.export_bins_by_tech[t] fix(m[:dvProductionToGrid][t, u, ts], 0.0, force=true) end - for b in p.s.storage.types.elec, u in p.export_bins_by_tech[t] + for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b] fix(m[:dvStorageToGrid][b, u, ts], 0.0, force=true) end end diff --git a/src/mpc/model.jl b/src/mpc/model.jl index e658e2737..60a2922df 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -90,7 +90,7 @@ function build_mpc!(m::JuMP.AbstractModel, p::MPCInputs) fix(m[:dvProductionToGrid][t, u, ts], 0.0, force=true) end - for b in p.s.storage.types.elec, u in p.export_bins_by_tech[t] + for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b] fix(m[:dvStorageToGrid][b, u, ts], 0.0, force=true) end end @@ -249,7 +249,7 @@ function add_variables!(m::JuMP.AbstractModel, p::MPCInputs) if !isempty(p.s.electric_tariff.export_bins) @variable(m, dvProductionToGrid[p.techs.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) - @variable(m, dvStorageToGrid[p.storage.types.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) + @variable(m, dvStorageToGrid[p.s.storage.types.elec, p.s.electric_tariff.export_bins, p.time_steps] >= 0) end m[:dvSize] = p.existing_sizes From a302d7f279492b637db6b36508648e0f4426a48b Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 2 Jun 2025 13:59:39 -0600 Subject: [PATCH 10/66] Update inputs.jl --- src/mpc/inputs.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/mpc/inputs.jl b/src/mpc/inputs.jl index 3aeece174..0be53266a 100644 --- a/src/mpc/inputs.jl +++ b/src/mpc/inputs.jl @@ -20,6 +20,8 @@ struct MPCInputs <: AbstractInputs ratchets::UnitRange techs_by_exportbin::DenseAxisArray{Array{String,1}} # indexed on [:NEM, :WHL] export_bins_by_tech::Dict{String, Array{Symbol, 1}} + storage_by_exportbin::DenseAxisArray{Array{String,1}} # indexed on [:NEM, :WHL] + export_bins_by_storage::Dict{String, Array{Symbol, 1}} cooling_cop::Dict{String, Array{Float64,1}} # (techs.cooling, time_steps) thermal_cop::Dict{String, Float64} # (techs.absorption_chiller) ghp_options::UnitRange{Int64} # Range of the number of GHP options @@ -64,6 +66,7 @@ function MPCInputs(s::MPCScenario) export_bins_by_tech[t] = s.electric_tariff.export_bins end # TODO implement export bins by tech (rather than assuming that all techs share the export_bins) + # TODO: account for storage export but only if Storage can_net_meter etc are true #Placeholder COP because the REopt model expects it cooling_cop = Dict("ExistingChiller" => ones(length(s.electric_load.loads_kw)) .* s.cooling_load.cop) @@ -92,6 +95,8 @@ function MPCInputs(s::MPCScenario) 1:length(s.electric_tariff.tou_demand_ratchet_time_steps), # ratchets techs_by_exportbin, export_bins_by_tech, + storage_by_exportbin, + export_bins_by_storage, cooling_cop, thermal_cop, ghp_options, From ae2f872eaf91b059a775a623e7c6672b7189e5f2 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:04:46 -0600 Subject: [PATCH 11/66] add todo --- src/core/reopt_inputs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index a3888e524..25c47089b 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -488,7 +488,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) cooling_cf["GHP"] = ones(length(time_steps)) end - ## HERE . add setup_electric_storage_inputs? For techs_by_exportbin. + ## TODO: HERE . add setup_electric_storage_inputs? For techs_by_exportbin. # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in for t in techs.elec From f823ae4bfe2f65b10fa655c4bcd236e2663791ef Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:36:14 -0600 Subject: [PATCH 12/66] Update CHANGELOG.md --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5993eb65b..91ad5dca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,7 @@ Classify the change according to the following categories: ## bess-export ### Added -- Add **ElectricStorage** input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` -- TODO remove: was: add input option **can_export_to_grid** (defaults to _false_) to `ElectricStorage` and decision variable **dvStorageToGrid** +- Add decision variable **dvStorageToGrid** and **ElectricStorage** input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` ## test-runners ### Added From 9322adfe2a0d2c2cf2f24b31c5f562560358bccd Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:56:16 -0600 Subject: [PATCH 13/66] condense 4 constraints into 1 --- src/constraints/storage_constraints.jl | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index a7d0ceb43..65bfef73c 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -113,26 +113,11 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") ) ) - # Constraint (4i)-1: Dispatch to electrical storage is no greater than power capacity - @constraint(m, [ts in p.time_steps], - m[Symbol("dvStoragePower"*_n)][b] >= - sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts] - ) - - #Constraint (4k)-alt: Dispatch to and from electrical storage is no greater than power capacity - @constraint(m, [ts in p.time_steps_with_grid], - m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] + - sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + m[Symbol("dvGridToStorage"*_n)][b, ts] - ) - - #Dispatch from electrical storage is no greater than power capacity - @constraint(m, [ts in p.time_steps_without_grid], - m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts]) - - #Constraint (4l)-alt: Dispatch from electrical storage is no greater than power capacity (no grid connection) - @constraint(m, [ts in p.time_steps_without_grid], - m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + - sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + #Constraint (4k): Dispatch to and from electrical storage is no greater than power capacity + @constraint(m, [ts in p.time_steps], + m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] + + sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) + + m[Symbol("dvGridToStorage"*_n)][b, ts] ) #Constraint (4m)-1: Remove grid-to-storage as an option if option to grid charge is turned off From 1e0b211a1684eb32606317c1980666da9e083095 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:04:45 -0600 Subject: [PATCH 14/66] Update storage_constraints.jl rm resolved TODOs --- src/constraints/storage_constraints.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index 65bfef73c..eb06bb245 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -74,11 +74,11 @@ end function add_elec_storage_dispatch_constraints(m, p, b; _n="") # Constraint (4d): Storage export must be less than total storage dispatch - if !isempty(p.export_bins_by_storage[b]) # TODO: Is this if statement needed? + if !isempty(p.export_bins_by_storage[b]) @constraint(m, [b in p.s.storage.attr[b], ts in p.time_steps_with_grid], sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for u in p.export_bins_by_storage[b]) <= - m[Symbol("dvDischargeFromStorage"*_n)][b, ts] # TODO: should this be divided by p.s.storage.attr[b].discharge_efficiency ? + m[Symbol("dvDischargeFromStorage"*_n)][b, ts] ) end From 7743683e6f3812f7cf24837bac6f5c90a9749337 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:06:28 -0600 Subject: [PATCH 15/66] rm commented out refs to can_export_to_grid --- src/core/energy_storage/electric_storage.jl | 3 --- src/mpc/structs.jl | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 7bb54026b..06b08875d 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -169,7 +169,6 @@ end soc_min_applies_during_outages::Bool = false soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true - # can_export_to_grid::Bool = false can_net_meter::Bool = false can_wholesale::Bool = false can_export_beyond_nem_limit = false @@ -209,7 +208,6 @@ Base.@kwdef struct ElectricStorageDefaults soc_min_applies_during_outages::Bool = false soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5 can_grid_charge::Bool = off_grid_flag ? false : true - # can_export_to_grid::Bool = false can_net_meter::Bool = false can_wholesale::Bool = false can_export_beyond_nem_limit = false @@ -255,7 +253,6 @@ struct ElectricStorage <: AbstractElectricStorage soc_min_applies_during_outages::Bool soc_init_fraction::Float64 can_grid_charge::Bool - # can_export_to_grid::Bool can_net_meter::Bool can_wholesale::Bool can_export_beyond_nem_limit::Bool diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index c90bf87ce..04aa5ac7c 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -226,7 +226,6 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - # can_export_to_grid::Bool = false can_net_meter::Bool = true can_wholesale::Bool = false can_export_beyond_nem_limit = false @@ -242,7 +241,6 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - # can_export_to_grid::Bool = false can_net_meter::Bool = true can_wholesale::Bool = false can_export_beyond_nem_limit = false From 48c270d1abfd3101fa4ec4728f86c3d06f61e56a Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:21:53 -0600 Subject: [PATCH 16/66] rm resolved TODOs on defining dvStorageToGrid --- src/core/reopt.jl | 3 --- src/mpc/model.jl | 1 - 2 files changed, 4 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index bd1f9a707..6883c8648 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -216,8 +216,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) @constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid], m[:dvProductionToStorage][b, t, ts] == 0) - # TODO: is the below needed? - @constraint(m, [u in p.export_bins_by_storage[b], ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, u, ts] == 0) elseif b in p.s.storage.types.hot @constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0) if "DomesticHotWater" in p.heating_loads_served_by_tes[b] @@ -643,7 +641,6 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) dvProductionToStorage[p.s.storage.types.all, union(p.techs.ghp,p.techs.all), p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] - # TODO determine if needed here: dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # export of energy from storage to the grid dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] dvStorageEnergy[p.s.storage.types.all] >= 0 # Energy capacity of storage system b [kWh] diff --git a/src/mpc/model.jl b/src/mpc/model.jl index 60a2922df..1fb4b1666 100644 --- a/src/mpc/model.jl +++ b/src/mpc/model.jl @@ -235,7 +235,6 @@ function add_variables!(m::JuMP.AbstractModel, p::MPCInputs) dvCurtail[p.techs.all, p.time_steps] >= 0 # [kW] dvProductionToStorage[p.s.storage.types.all, p.techs.all, p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW] dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW] - # dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW] dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW] From 041d7596cc0244c86dcee860bcca443c4c3d7a5d Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 11 Jun 2025 14:30:47 -0600 Subject: [PATCH 17/66] rmv extra constraint --- src/core/reopt.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index bd1f9a707..f99c4c026 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -216,8 +216,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) @constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0) @constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid], m[:dvProductionToStorage][b, t, ts] == 0) - # TODO: is the below needed? - @constraint(m, [u in p.export_bins_by_storage[b], ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, u, ts] == 0) elseif b in p.s.storage.types.hot @constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0) if "DomesticHotWater" in p.heating_loads_served_by_tes[b] From cd67b3ff6fa73ccfd0ce1df77e63fd5839f0034b Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 11 Jun 2025 14:34:15 -0600 Subject: [PATCH 18/66] updt CUR to EXC --- src/core/reopt_inputs.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 25c47089b..64c1201be 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -33,9 +33,9 @@ struct REoptInputs <: AbstractInputs maxsize_pv_locations::DenseAxisArray{<:Real, 1} # indexed on pvlocations pv_to_location::Dict{String, Dict{Symbol, Int64}} # (techs.pv, pvlocations) ratchets::UnitRange - techs_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :CUR] + techs_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :EXC] export_bins_by_tech::Dict - storage_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL] + storage_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :EXC] export_bins_by_storage::Dict n_segs_by_tech::Dict{String, Int} seg_min_size::Dict{String, Dict{Int, <:Real}} @@ -100,9 +100,9 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs maxsize_pv_locations::DenseAxisArray{<:Real, 1} # indexed on pvlocations pv_to_location::Dict{String, Dict{Symbol, Int64}} # (techs.pv, pvlocations) ratchets::UnitRange - techs_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :CUR] + techs_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :EXC] export_bins_by_tech::Dict - storage_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL] + storage_by_exportbin::Dict{Symbol, AbstractArray} # keys can include [:NEM, :WHL, :EXC] export_bins_by_storage::Dict n_segs_by_tech::Dict{String, Int} seg_min_size::Dict{String, Dict{Int, Real}} From 42a4937f8d08325f92a0ad3702ad681f00e70fe6 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 11 Jun 2025 14:37:19 -0600 Subject: [PATCH 19/66] rmv todos and infos --- src/core/reopt_inputs.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 64c1201be..9c5209d8b 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -488,8 +488,6 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) cooling_cf["GHP"] = ones(length(time_steps)) end - ## TODO: HERE . add setup_electric_storage_inputs? For techs_by_exportbin. - # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] @@ -499,8 +497,6 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) for b in s.storage.types.elec fillin_storage_by_exportbin(s, storage_by_exportbin, b) export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] - @info "export_bins_by_storage: $(export_bins_by_storage)" - @info "storage_by_exportbin: $(storage_by_exportbin)" end end From 827647adb6dfa6659ebbef7a0dce7d519bf108d8 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:54:47 -0600 Subject: [PATCH 20/66] add todos, rm unneeded if --- src/core/reopt_inputs.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 25c47089b..8e244d5ac 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -495,13 +495,13 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] end - if !isempty(s.storage.types.elec) - for b in s.storage.types.elec - fillin_storage_by_exportbin(s, storage_by_exportbin, b) - export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] - @info "export_bins_by_storage: $(export_bins_by_storage)" - @info "storage_by_exportbin: $(storage_by_exportbin)" - end + for b in s.storage.types.elec + #TODO: wrap setting storage_by_exportbin and export_bins_by_storage into one function (don't need to separate like techs) + fillin_storage_by_exportbin(s, storage_by_exportbin, b) + export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] + @info "export_bins_by_storage: $(export_bins_by_storage)" + @info "storage_by_exportbin: $(storage_by_exportbin)" + #TODO: remove infos end if s.settings.off_grid_flag From 547d1bdd64235782c403110e159f5943464a6b05 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Thu, 12 Jun 2025 11:55:27 -0600 Subject: [PATCH 21/66] MPC storage export --- src/mpc/inputs.jl | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/mpc/inputs.jl b/src/mpc/inputs.jl index 0be53266a..491ec5a5c 100644 --- a/src/mpc/inputs.jl +++ b/src/mpc/inputs.jl @@ -52,6 +52,31 @@ function MPCInputs(s::MPCScenario) techs_by_exportbin = DenseAxisArray([ techs.all, techs.all, techs.all], s.electric_tariff.export_bins) # TODO account for which techs have access to export bins (when we add more techs than PV) + export_bins_by_tech = Dict{String, Array{Symbol, 1}}() + for t in techs.elec + export_bins_by_tech[t] = s.electric_tariff.export_bins + end + # TODO implement export bins by tech (rather than assuming that all techs share the export_bins) + + storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) + export_bins_by_storage = Dict{String, Array{Symbol, 1}}() + for b in s.storage.types.elec + if s.storage.attr[b].can_net_meter && :NEM in keys(storage_by_exportbin) + push!(storage_by_exportbin[:NEM], b) + if s.storage.attr[b].can_export_beyond_nem_limit && :EXC in keys(storage_by_exportbin) + push!(storage_by_exportbin[:EXC], b) + end + end + if s.storage.attr[b].can_wholesale && :WHL in keys(storage_by_exportbin) + push!(storage_by_exportbin[:WHL], b) + end + + export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] + #TODO: remove infos + @info "export_bins_by_storage: $(export_bins_by_storage)" + @info "storage_by_exportbin: $(storage_by_exportbin)" + end + levelization_factor = Dict(t => 1.0 for t in techs.all) pwf_e = 1.0 pwf_om = 1.0 @@ -61,13 +86,6 @@ function MPCInputs(s::MPCScenario) time_steps_with_grid, time_steps_without_grid, = setup_electric_utility_inputs(s) - export_bins_by_tech = Dict{String, Array{Symbol, 1}}() - for t in techs.elec - export_bins_by_tech[t] = s.electric_tariff.export_bins - end - # TODO implement export bins by tech (rather than assuming that all techs share the export_bins) - # TODO: account for storage export but only if Storage can_net_meter etc are true - #Placeholder COP because the REopt model expects it cooling_cop = Dict("ExistingChiller" => ones(length(s.electric_load.loads_kw)) .* s.cooling_load.cop) thermal_cop = Dict{String, Float64}() From 7da2839e426c2ded6c88d2b7462bba222b72584b Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:21:52 -0600 Subject: [PATCH 22/66] replace NEM_techs & WHL_techs w techs_by_exportbin elem, same for storage vs --- .../electric_utility_constraints.jl | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 29fca396c..c7af39158 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -13,29 +13,25 @@ function add_export_constraints(m, p; _n="") NEM_benefit = 0 EXC_benefit = 0 WHL_benefit = 0 - NEM_techs = String[t for t in p.techs.elec if :NEM in p.export_bins_by_tech[t]] - WHL_techs = String[t for t in p.techs.elec if :WHL in p.export_bins_by_tech[t]] - NEM_storage = String[b for b in p.s.storage.types.elec if :NEM in p.export_bins_by_storage[b]] - WHL_storage = String[b for b in p.s.storage.types.elec if :WHL in p.export_bins_by_storage[b]] - - if !isempty(vcat(NEM_techs, NEM_storage)) + + if !isempty(vcat(techs_by_exportbin[:NEM], storage_by_exportbin[:NEM])) # Constraint (9c): Net metering only -- can't sell more than you purchase # hours_per_time_step is cancelled on both sides, but used for unit consistency (convert power to energy) @constraint(m, p.hours_per_time_step * ( - sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in NEM_techs, ts in p.time_steps) + - sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in NEM_storage, ts in p.time_steps) + sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in techs_by_exportbin[:NEM], ts in p.time_steps) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in storage_by_exportbin[:NEM], ts in p.time_steps) ) <= p.hours_per_time_step * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) ) - if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(vcat(WHL_techs, WHL_storage)) + if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(vcat(techs_by_exportbin[:WHL], storage_by_exportbin[:WHL])) # no need for binNEM nor binWHL binNEM = 1 # TODO: Decide whether to ignore BESS size in this constraint? @constraint(m, - sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw + sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= p.s.electric_utility.interconnection_limit_kw ) NEM_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * sum(p.s.electric_tariff.export_rates[:NEM][ts] * ( @@ -68,17 +64,17 @@ function add_export_constraints(m, p; _n="") if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) # TODO: Decide whether to ignore BESS size in these constraints? @constraint(m, - binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.net_metering_limit_kw} + binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= p.s.electric_utility.net_metering_limit_kw} ) @constraint(m, - !binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw} + !binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= p.s.electric_utility.interconnection_limit_kw} ) else #leverage max system sizes for interconnect limit size, alternate is max monthly fully-electrified load in kWh #assume electric heater with COP of 1 for conversion of heat to electricity max_interconnection_size = minimum([ p.s.electric_utility.interconnection_limit_kw, - sum(p.max_sizes[t] for t in NEM_techs), + sum(p.max_sizes[t] for t in techs_by_exportbin[:NEM]), p.hours_per_time_step * maximum([sum(( p.s.electric_load.loads_kw[ts] + p.s.cooling_load.loads_kw_thermal[ts]/p.cooling_cop["ExistingChiller"][ts] + @@ -88,7 +84,7 @@ function add_export_constraints(m, p; _n="") ]) @constraint(m, - sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= max_interconnection_size - (max_interconnection_size - p.s.electric_utility.net_metering_limit_kw)*binNEM + sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= max_interconnection_size - (max_interconnection_size - p.s.electric_utility.net_metering_limit_kw)*binNEM ) end @@ -161,7 +157,7 @@ function add_export_constraints(m, p; _n="") end end - if !isempty(vcat(WHL_techs, WHL_storage)) + if !isempty(vcat(techs_by_exportbin[:WHL], storage_by_exportbin[:WHL])) if typeof(binNEM) <: Real # no need for wholesale binary binWHL = 1 From e19efe65fe70f8cdad5e1299c22f6058f16b7441 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:28:30 -0600 Subject: [PATCH 23/66] remove infos --- src/mpc/inputs.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/mpc/inputs.jl b/src/mpc/inputs.jl index 491ec5a5c..26b7e8392 100644 --- a/src/mpc/inputs.jl +++ b/src/mpc/inputs.jl @@ -72,9 +72,6 @@ function MPCInputs(s::MPCScenario) end export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] - #TODO: remove infos - @info "export_bins_by_storage: $(export_bins_by_storage)" - @info "storage_by_exportbin: $(storage_by_exportbin)" end levelization_factor = Dict(t => 1.0 for t in techs.all) From 61aba3575b2be16d4aa3969d8bb7dd11fd05cb72 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:34:36 -0600 Subject: [PATCH 24/66] Revert "replace NEM_techs & WHL_techs w techs_by_exportbin elem, same for storage vs" This reverts commit 7da2839e426c2ded6c88d2b7462bba222b72584b. --- .../electric_utility_constraints.jl | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index c7af39158..29fca396c 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -13,25 +13,29 @@ function add_export_constraints(m, p; _n="") NEM_benefit = 0 EXC_benefit = 0 WHL_benefit = 0 - - if !isempty(vcat(techs_by_exportbin[:NEM], storage_by_exportbin[:NEM])) + NEM_techs = String[t for t in p.techs.elec if :NEM in p.export_bins_by_tech[t]] + WHL_techs = String[t for t in p.techs.elec if :WHL in p.export_bins_by_tech[t]] + NEM_storage = String[b for b in p.s.storage.types.elec if :NEM in p.export_bins_by_storage[b]] + WHL_storage = String[b for b in p.s.storage.types.elec if :WHL in p.export_bins_by_storage[b]] + + if !isempty(vcat(NEM_techs, NEM_storage)) # Constraint (9c): Net metering only -- can't sell more than you purchase # hours_per_time_step is cancelled on both sides, but used for unit consistency (convert power to energy) @constraint(m, p.hours_per_time_step * ( - sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in techs_by_exportbin[:NEM], ts in p.time_steps) + - sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in storage_by_exportbin[:NEM], ts in p.time_steps) + sum( m[Symbol("dvProductionToGrid"*_n)][t, :NEM, ts] for t in NEM_techs, ts in p.time_steps) + + sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in NEM_storage, ts in p.time_steps) ) <= p.hours_per_time_step * sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) ) - if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(vcat(techs_by_exportbin[:WHL], storage_by_exportbin[:WHL])) + if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(vcat(WHL_techs, WHL_storage)) # no need for binNEM nor binWHL binNEM = 1 # TODO: Decide whether to ignore BESS size in this constraint? @constraint(m, - sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= p.s.electric_utility.interconnection_limit_kw + sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw ) NEM_benefit = @expression(m, p.pwf_e * p.hours_per_time_step * sum(p.s.electric_tariff.export_rates[:NEM][ts] * ( @@ -64,17 +68,17 @@ function add_export_constraints(m, p; _n="") if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) # TODO: Decide whether to ignore BESS size in these constraints? @constraint(m, - binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= p.s.electric_utility.net_metering_limit_kw} + binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.net_metering_limit_kw} ) @constraint(m, - !binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= p.s.electric_utility.interconnection_limit_kw} + !binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw} ) else #leverage max system sizes for interconnect limit size, alternate is max monthly fully-electrified load in kWh #assume electric heater with COP of 1 for conversion of heat to electricity max_interconnection_size = minimum([ p.s.electric_utility.interconnection_limit_kw, - sum(p.max_sizes[t] for t in techs_by_exportbin[:NEM]), + sum(p.max_sizes[t] for t in NEM_techs), p.hours_per_time_step * maximum([sum(( p.s.electric_load.loads_kw[ts] + p.s.cooling_load.loads_kw_thermal[ts]/p.cooling_cop["ExistingChiller"][ts] + @@ -84,7 +88,7 @@ function add_export_constraints(m, p; _n="") ]) @constraint(m, - sum(m[Symbol("dvSize"*_n)][t] for t in techs_by_exportbin[:NEM]) <= max_interconnection_size - (max_interconnection_size - p.s.electric_utility.net_metering_limit_kw)*binNEM + sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= max_interconnection_size - (max_interconnection_size - p.s.electric_utility.net_metering_limit_kw)*binNEM ) end @@ -157,7 +161,7 @@ function add_export_constraints(m, p; _n="") end end - if !isempty(vcat(techs_by_exportbin[:WHL], storage_by_exportbin[:WHL])) + if !isempty(vcat(WHL_techs, WHL_storage)) if typeof(binNEM) <: Real # no need for wholesale binary binWHL = 1 From 1d05bec6e6dff19d37a5c517568082287a243fd7 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:38:09 -0600 Subject: [PATCH 25/66] rm todo --- src/core/reopt_inputs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index bc6b99a4e..4f6631d49 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -382,7 +382,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) @info "s.electric_tariff.export_bins: $(s.electric_tariff.export_bins)" techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) export_bins_by_tech = Dict{String, Array{Symbol, 1}}() - storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) # TODO remove CUR? + storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) export_bins_by_storage = Dict{String, Array{Symbol, 1}}() # REoptInputs indexed on techs.segmented From 335acee35b853164f6d3bead052aafab1b7f4f23 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:25:37 -0600 Subject: [PATCH 26/66] reformat RE sums/comments for clarity --- .../renewable_energy_constraints.jl | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 4c8bb1003..294f918e9 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -61,20 +61,24 @@ function add_re_elec_calcs(m,p) # Note: when we add capability for battery to discharge to grid, need to make sure only RE that is being consumed # onsite is counted so battery doesn't become a back door for RE to grid. m[:AnnualOnsiteREEleckWh] = @expression(m, p.hours_per_time_step * ( + #total RE elec generation, excl steam turbine sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[:dvRatedProduction][t,ts] * p.tech_renewable_energy_fraction[t] for t in setdiff(p.techs.elec, p.techs.steam_turbine), ts in p.time_steps - ) - #total RE elec generation, excl steam turbine - sum(m[:dvProductionToStorage][b,t,ts]*p.tech_renewable_energy_fraction[t]*( + ) + #minus battery efficiency losses + - sum(m[:dvProductionToStorage][b,t,ts]*p.tech_renewable_energy_fraction[t]*( 1-p.s.storage.attr[b].charge_efficiency*p.s.storage.attr[b].discharge_efficiency) for t in setdiff(p.techs.elec, p.techs.steam_turbine), b in p.s.storage.types.elec, ts in p.time_steps - ) - #minus battery efficiency losses - sum(m[:dvCurtail][t,ts] * p.tech_renewable_energy_fraction[t] + ) + # minus curtailment + - sum(m[:dvCurtail][t,ts] * p.tech_renewable_energy_fraction[t] for t in setdiff(p.techs.elec, p.techs.steam_turbine), ts in p.time_steps - ) - # minus curtailment - (1 - p.s.site.include_exported_renewable_electricity_in_total) * + ) + # minus exported RE, if RE accounting method = 0. + - (1 - p.s.site.include_exported_renewable_electricity_in_total) * sum(m[:dvProductionToGrid][t,u,ts]*p.tech_renewable_energy_fraction[t] for t in setdiff(p.techs.elec, p.techs.steam_turbine), u in p.export_bins_by_tech[t], ts in p.time_steps - ) # minus exported RE, if RE accounting method = 0. + ) ) # + SteamTurbineAnnualREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step ) @@ -82,12 +86,14 @@ function add_re_elec_calcs(m,p) # Note: when we add capability for battery to discharge to grid, need to subtract out *grid RE* discharged from battery # back to grid so that loop doesn't become a back door for increasing RE. This will require some careful thought! m[:AnnualGridREEleckWh] = @expression(m, p.hours_per_time_step * ( + # renewable energy from grid sum(m[:dvGridPurchase][ts, tier] * p.s.electric_utility.renewable_energy_fraction_series[ts] - for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) # renewable energy from grid + for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) + # minus battery efficiency losses from grid charging storage (assumes all that is charged is discharged) - sum(m[:dvGridToStorage][b, ts] * p.s.electric_utility.renewable_energy_fraction_series[ts] * (1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) for ts in p.time_steps, b in p.s.storage.types.elec - ) # minus battery efficiency losses from grid charging storage (assumes all that is charged is discharged) + ) ) ) From 056106f66d36ea10cb7900a77d490013923e43c7 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:32:23 -0600 Subject: [PATCH 27/66] fix MPC convert error --- src/mpc/inputs.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mpc/inputs.jl b/src/mpc/inputs.jl index 26b7e8392..d020e4aad 100644 --- a/src/mpc/inputs.jl +++ b/src/mpc/inputs.jl @@ -58,7 +58,7 @@ function MPCInputs(s::MPCScenario) end # TODO implement export bins by tech (rather than assuming that all techs share the export_bins) - storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) + storage_by_exportbin = Dict{Symbol, Array{String, 1}}(k => [] for k in s.electric_tariff.export_bins) export_bins_by_storage = Dict{String, Array{Symbol, 1}}() for b in s.storage.types.elec if s.storage.attr[b].can_net_meter && :NEM in keys(storage_by_exportbin) @@ -73,6 +73,7 @@ function MPCInputs(s::MPCScenario) export_bins_by_storage[b] = [bin for (bin, ts) in storage_by_exportbin if b in ts] end + storage_by_exportbin = DenseAxisArray(collect(values(storage_by_exportbin)), collect(keys(storage_by_exportbin))) levelization_factor = Dict(t => 1.0 for t in techs.all) pwf_e = 1.0 From fae0ff8440a82f34edde348c634b53e5600aaa3f Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 17 Jun 2025 11:50:52 -0600 Subject: [PATCH 28/66] combine 2 tests --- test/runtests.jl | 50 +++++++++++++++++++----------------------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 26d771767..c81fb0579 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -129,23 +129,39 @@ else # run HiGHS tests @test wh_min_allowable_size ≈ 5.0 atol=1e-8 end - @testset "January Export Rates" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + @testset "Export Rate greater than retail rate" begin data = JSON.parsefile("./scenarios/monthly_rate.json") - # create wholesale_rate with compensation in January > retail rate + # 1) create wholesale_rate with compensation in January > retail rate + # and check that PV exports instead of serving load jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] data["ElectricTariff"]["wholesale_rate"] = append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) s = Scenario(data) inputs = REoptInputs(s) results = run_reopt(model, inputs) @test results["PV"]["size_kw"] ≈ 68.9323 atol=0.01 @test results["Financial"]["lcc"] ≈ 432681.26 rtol=1e-5 # with levelization_factor hack the LCC is within 5e-5 of REopt API LCC - @test all(x == 0.0 for x in results["PV"]["electric_to_load_series_kw"][1:744]) + @test all(x == 0.0 for x in results["PV"]["electric_to_load_series_kw"][1:(31*24)]) + finalize(backend(model)) + empty!(model) + GC.gc() + + # 2) now don't allow simultaneous export/import and check that + # PV does NOT export unless the site load is met first for the month of January + data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false) + + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(model, inputs) + + @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"][1:744]) + if results["PV"]["electric_to_grid_series_kw"][i] > 0) finalize(backend(model)) empty!(model) GC.gc() @@ -1142,32 +1158,6 @@ else # run HiGHS tests end end - #= - add a time-of-export rate that is greater than retail rate for the month of January, - check to make sure that PV does NOT export unless the site load is met first for the month of January. - =# - @testset "Do not allow_simultaneous_export_import" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - data = JSON.parsefile("./scenarios/monthly_rate.json") - - # create wholesale_rate with compensation in January > retail rate - jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] - data["ElectricTariff"]["wholesale_rate"] = - append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) - data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) - data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false) - - s = Scenario(data) - inputs = REoptInputs(s) - results = run_reopt(model, inputs) - - @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"][1:744]) - if results["PV"]["electric_to_grid_series_kw"][i] > 0) - finalize(backend(model)) - empty!(model) - GC.gc() - end - #= Battery degradation replacement strategy test can be validated against solvers like Xpress. Commented out of this testset due to solve time constraints using open-source solvers. From 5c31eeb72614f11d7fe40b5d4f6c0d0f77131d42 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:47:40 -0600 Subject: [PATCH 29/66] correct constraint (4d) loop --- src/constraints/storage_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index eb06bb245..cdf50baae 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -75,7 +75,7 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") # Constraint (4d): Storage export must be less than total storage dispatch if !isempty(p.export_bins_by_storage[b]) - @constraint(m, [b in p.s.storage.attr[b], ts in p.time_steps_with_grid], + @constraint(m, [ts in p.time_steps_with_grid], sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for u in p.export_bins_by_storage[b]) <= m[Symbol("dvDischargeFromStorage"*_n)][b, ts] From f45fe815d39b7548a962b0bf846c52bc5ed70310 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:48:45 -0600 Subject: [PATCH 30/66] Update runtests.jl --- test/runtests.jl | 58 ++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index c81fb0579..dc66533aa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -129,7 +129,7 @@ else # run HiGHS tests @test wh_min_allowable_size ≈ 5.0 atol=1e-8 end - @testset "Export Rate greater than retail rate" begin + @testset "Export rate greater than retail rate" begin data = JSON.parsefile("./scenarios/monthly_rate.json") # 1) create wholesale_rate with compensation in January > retail rate @@ -725,33 +725,37 @@ else # run HiGHS tests end end - @testset verbose=true "Net Metering" begin - @testset "Net Metering Limit and Wholesale" begin - #case 1: net metering limit is met by PV - d = JSON.parsefile("./scenarios/net_metering.json") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["PV"]["size_kw"] ≈ 30.0 atol=1e-3 - - #case 2: wholesale rate is high, big-M is met - d["ElectricTariff"]["wholesale_rate"] = 5.0 - d["PV"]["can_wholesale"] = true - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["PV"]["size_kw"] ≈ 7440.0 atol=1e-3 #max benefit provides the upper bound + @testset verbose=true "Net Metering Limit and Wholesale" begin + #case 1: net metering limit is met by PV + d = JSON.parsefile("./scenarios/net_metering.json") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["PV"]["size_kw"] ≈ 30.0 atol=1e-3 + finalize(backend(m)) + empty!(m) + GC.gc() + + #case 2: wholesale rate is high, big-M is met + d["ElectricTariff"]["wholesale_rate"] = 5.0 + d["PV"]["can_wholesale"] = true + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["PV"]["size_kw"] ≈ 7440.0 atol=1e-3 #max benefit provides the upper bound + finalize(backend(m)) + empty!(m) + GC.gc() - #case 3: net metering limit is exceeded, no WHL, and min RE % - d["ElectricTariff"]["wholesale_rate"] = 0 - d["PV"]["min_kw"] = 50 - d["Site"]["renewable_electricity_min_fraction"] = 0.35 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test sum(results["PV"]["electric_to_grid_series_kw"]) ≈ 0.0 atol=1e-3 - @test results["ElectricTariff"]["lifecycle_export_benefit_after_tax"] ≈ 0.0 atol=1e-3 - finalize(backend(m)) - empty!(m) - GC.gc() - end + #case 3: net metering limit is exceeded, no WHL, and min RE % + d["ElectricTariff"]["wholesale_rate"] = 0 + d["PV"]["min_kw"] = 50 + d["Site"]["renewable_electricity_min_fraction"] = 0.35 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test sum(results["PV"]["electric_to_grid_series_kw"]) ≈ 0.0 atol=1e-3 + @test results["ElectricTariff"]["lifecycle_export_benefit_after_tax"] ≈ 0.0 atol=1e-3 + finalize(backend(m)) + empty!(m) + GC.gc() end @testset "Heating loads and addressable load fraction" begin From 55ce586474a5ba64db04c383fa459e5c43543229 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 17 Jun 2025 14:49:11 -0600 Subject: [PATCH 31/66] start bess export testing --- test/development_tests.jl | 66 + test/runtests.jl | 4 + test/scenarios/bess_export.json | 8805 +++++++++++++++++++++++++++++++ 3 files changed, 8875 insertions(+) create mode 100644 test/development_tests.jl create mode 100644 test/scenarios/bess_export.json diff --git a/test/development_tests.jl b/test/development_tests.jl new file mode 100644 index 000000000..72d988a78 --- /dev/null +++ b/test/development_tests.jl @@ -0,0 +1,66 @@ +@testset verbose=true "Battery Can Export" begin + + # case where whl rate is between off peak and on peak retail rates? + # or whl rate > retail rate and allow_simultaneous_export_import = false (copy existing test) + + # Case 1: energy rate is lower during PV production + # so expect battery to do energy arbitrage and NEM export during higher rate + d = JSON.parsefile("./scenarios/bess_export.json") + d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.2, 0.1, 0.2], inner=8, outer=365) + # d["ElectricStorage"]["can_net_meter"] = true + # d["ElectricStorage"]["can_wholesale"] = true + # d["PV"]["can_net_meter"] = false + # d["PV"]["can_wholesale"] = false + p = REoptInputs(d) + for exbin in [:WHL, :NEM] + @test exbin in p.export_bins_by_storage["ElectricStorage"] + end + @test !(:EXC in p.export_bins_by_storage["ElectricStorage"]) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + open("debug_results.json","w") do f + JSON.print(f, results, 4) + end + # @test results["PV"]["electric_to_grid_series_kw"] + println(sum(results["ElectricStorage"]["electric_to_grid_series_kw"])) + @test sum(results["ElectricStorage"]["electric_to_grid_series_kw"]) > 0 + @test m[:WHL_benefit] == 0 + @test m[:EXC_benefit] == 0 + @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 + finalize(backend(m)) + empty!(m) + GC.gc() + + # Case 2: whl rate > retail rate and allow_simultaneous_export_import = false + # so expect battery to wholesale once load is met + #TODO: have to limit PV size to get expected result? + d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.1], outer=8760) + d["ElectricTariff"]["wholesale_rate"] = 0.2 + d["ElectricTariff"]["allow_simultaneous_export_import"] = false + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"]) + if results["ElectricStorage"]["electric_to_grid_series_kw"][i] > 0) + @test sum(results["ElectricStorage"]["electric_to_grid_series_kw"]) > 0 + @test m[:NEM_benefit] == 0 + @test m[:EXC_benefit] == 0 + @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 + finalize(backend(m)) + empty!(m) + GC.gc() + + # More testing of storage export inputs + # d["ElectricStorage"]["can_net_meter"] = true + # d["ElectricStorage"]["can_wholesale"] = true + d["ElectricStorage"]["can_export_beyond_nem_limit"] = true + p = REoptInputs(d) + println(p.export_bins_by_storage["ElectricStorage"]) + for exbin in [:EXC, :WHL, :NEM] + @test exbin in p.export_bins_by_storage["ElectricStorage"] + end + d["ElectricStorage"]["can_net_meter"] = false + d["ElectricStorage"]["can_wholesale"] = false + d["ElectricStorage"]["can_export_beyond_nem_limit"] = false + p = REoptInputs(d) + @test isempty(p.export_bins_by_storage["ElectricStorage"]) +end diff --git a/test/runtests.jl b/test/runtests.jl index dc66533aa..ca5dfab0e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,6 +23,10 @@ elseif "CPLEX" in ARGS @testset "test_with_cplex" begin include("test_with_cplex.jl") end +elseif "Dev" in ARGS + @testset "test development" begin + include("development_tests.jl") + end else # run HiGHS tests @testset verbose=true "REopt test set using HiGHS solver" begin @testset "Prevent simultaneous charge and discharge" begin diff --git a/test/scenarios/bess_export.json b/test/scenarios/bess_export.json new file mode 100644 index 000000000..cf4e339f4 --- /dev/null +++ b/test/scenarios/bess_export.json @@ -0,0 +1,8805 @@ +{ + "Settings": { + "solver_name": "HiGHS" + }, + "Site": { + "longitude": -118.1164613, + "latitude": 34.5794343, + "land_acres": 1000000.0, + "roof_squarefeet": 0 + }, + "Financial": { + "elec_cost_escalation_rate_fraction": 0.02 + }, + "ElectricLoad": { + "annual_kwh": 87600.0, + "doe_reference_name": "FlatLoad" + }, + "ElectricTariff": { + "wholesale_rate": 0.05 + }, + "ElectricUtility": { + "net_metering_limit_kw": 100000 + }, + "PV": { + "array_type": 0, + "max_kw": 30, + "can_net_meter": true, + "can_wholesale": true, + "can_export_beyond_nem_limit": true, + "federal_itc_fraction": 0.3, + "macrs_option_years": 5, + "macrs_bonus_fraction": 0.4, + "module_type": 0, + "om_cost_per_kw": 25.0, + "production_factor_series": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000518, + 0.056613, + 0.118279, + 0.308885, + 0.039057, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.02598, + 0.03948, + 0.016744, + 0.005671, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000559, + 0.011742, + 0.011735, + 0.004853, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006824, + 0.023271, + 0.024333, + 0.011057, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.031792, + 0.030591, + 0.131599, + 0.035552, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.061548, + 0.145051, + 0.047602, + 0.074433, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004269, + 0.013417, + 0.014395, + 0.00531, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.030892, + 0.013711, + 0.014525, + 0.007344, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004622, + 0.008666, + 0.168508, + 0.329871, + 0.299311, + 0.107119, + 0.003722, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002098, + 0.089462, + 0.158831, + 0.277314, + 0.242311, + 0.054668, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003572, + 0.015286, + 0.016205, + 0.009714, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008545, + 0.019967, + 0.021989, + 0.013398, + 0.077699, + 0.002807, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010271, + 0.016279, + 0.01707, + 0.013133, + 0.057014, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014186, + 0.175443, + 0.028953, + 0.019303, + 0.140805, + 0.023916, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001729, + 0.007616, + 0.009716, + 0.010416, + 0.007475, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015557, + 0.023251, + 0.013242, + 0.01022, + 0.002816, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007976, + 0.014561, + 0.028306, + 0.010887, + 0.001951, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015027, + 0.027091, + 0.015819, + 0.020577, + 0.002332, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01383, + 0.196343, + 0.287024, + 0.282807, + 0.100937, + 0.003881, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004235, + 0.02219, + 0.034408, + 0.037642, + 0.405648, + 0.209585, + 0.047132, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.035184, + 0.167966, + 0.237212, + 0.354362, + 0.36839, + 0.186179, + 0.021769, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017704, + 0.019855, + 0.033493, + 0.0353, + 0.026872, + 0.008006, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00136, + 0.019044, + 0.031095, + 0.032866, + 0.024539, + 0.008183, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001894, + 0.01924, + 0.031375, + 0.033148, + 0.024602, + 0.247833, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.053569, + 0.209102, + 0.295357, + 0.419706, + 0.443353, + 0.278818, + 0.048048, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004125, + 0.016475, + 0.181703, + 0.046132, + 0.483709, + 0.017736, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005792, + 0.016131, + 0.044225, + 0.047133, + 0.037516, + 0.01811, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005825, + 0.027536, + 0.040385, + 0.043226, + 0.024782, + 0.016243, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.052317, + 0.190949, + 0.268233, + 0.353349, + 0.356242, + 0.217694, + 0.00185, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.050423, + 0.090416, + 0.035094, + 0.037812, + 0.055791, + 0.106181, + 0.04806, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.0063, + 0.018298, + 0.026662, + 0.022789, + 0.022643, + 0.009209, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.100095, + 0.24846, + 0.328843, + 0.048922, + 0.141418, + 0.095619, + 0.079047, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017991, + 0.092172, + 0.033399, + 0.046713, + 0.446491, + 0.315533, + 0.080925, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.096118, + 0.26739, + 0.374576, + 0.483593, + 0.492427, + 0.374571, + 0.115578, + 0.006301, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.020525, + 0.032929, + 0.046184, + 0.048893, + 0.040766, + 0.023169, + 0.002807, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.145376, + 0.304623, + 0.396495, + 0.496212, + 0.330993, + 0.276183, + 0.112042, + 0.010179, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.0047, + 0.157231, + 0.292534, + 0.380971, + 0.467121, + 0.489697, + 0.358376, + 0.124434, + 0.005228, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000218, + 0.019237, + 0.150025, + 0.183635, + 0.045047, + 0.038466, + 0.057669, + 0.008409, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.026635, + 0.037269, + 0.043151, + 0.043682, + 0.041081, + 0.033318, + 0.012948, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000357, + 0.02176, + 0.042365, + 0.056804, + 0.059425, + 0.050497, + 0.032009, + 0.008504, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000747, + 0.024504, + 0.04612, + 0.06006, + 0.062924, + 0.054245, + 0.035796, + 0.010089, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001599, + 0.021092, + 0.041544, + 0.283667, + 0.220936, + 0.300114, + 0.083573, + 0.16323, + 0.006269, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001182, + 0.031332, + 0.05407, + 0.069096, + 0.07133, + 0.062593, + 0.04321, + 0.015723, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003903, + 0.027536, + 0.036454, + 0.048542, + 0.051232, + 0.043102, + 0.032407, + 0.019759, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001244, + 0.015733, + 0.02802, + 0.03633, + 0.518635, + 0.032292, + 0.022169, + 0.008504, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003138, + 0.01954, + 0.034173, + 0.197684, + 0.508296, + 0.498743, + 0.378368, + 0.195993, + 0.00898, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.072697, + 0.217068, + 0.228148, + 0.069537, + 0.276857, + 0.384884, + 0.438048, + 0.25008, + 0.022959, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010438, + 0.028435, + 0.050027, + 0.062996, + 0.065483, + 0.056526, + 0.039259, + 0.020245, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013452, + 0.02973, + 0.048195, + 0.061199, + 0.063713, + 0.054801, + 0.037655, + 0.022758, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010772, + 0.036665, + 0.060666, + 0.074795, + 0.078158, + 0.50409, + 0.478434, + 0.278539, + 0.03924, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.012676, + 0.037731, + 0.060701, + 0.075988, + 0.078465, + 0.069425, + 0.048724, + 0.023507, + 0.000934, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014304, + 0.088999, + 0.061627, + 0.076929, + 0.079382, + 0.069393, + 0.049815, + 0.02403, + 0.001748, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000311, + 0.121746, + 0.049438, + 0.074089, + 0.28787, + 0.566797, + 0.630167, + 0.295748, + 0.032341, + 0.001237, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.037268, + 0.164169, + 0.237277, + 0.438479, + 0.481457, + 0.410996, + 0.367103, + 0.057306, + 0.007962, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016857, + 0.041108, + 0.16158, + 0.078969, + 0.351808, + 0.071522, + 0.053229, + 0.027137, + 0.00365, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.022274, + 0.037971, + 0.059556, + 0.072955, + 0.07605, + 0.040739, + 0.047833, + 0.027856, + 0.006644, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.044246, + 0.040437, + 0.062105, + 0.075483, + 0.078199, + 0.068501, + 0.050349, + 0.02834, + 0.009016, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001877, + 0.087798, + 0.10187, + 0.32886, + 0.505933, + 0.473687, + 0.064377, + 0.04979, + 0.037566, + 0.013176, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.029999, + 0.061335, + 0.086721, + 0.22403, + 0.1313, + 0.137944, + 0.219265, + 0.044405, + 0.010796, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017662, + 0.013159, + 0.298859, + 0.040957, + 0.596558, + 0.663549, + 0.612808, + 0.506399, + 0.332935, + 0.116517, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 7.7e-05, + 0.015269, + 0.380016, + 0.529256, + 0.632799, + 0.652133, + 0.59478, + 0.468261, + 0.29569, + 0.094786, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.023546, + 0.229046, + 0.448351, + 0.611599, + 0.695916, + 0.705675, + 0.651943, + 0.523266, + 0.33848, + 0.125267, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.032318, + 0.260217, + 0.475973, + 0.635273, + 0.719629, + 0.736266, + 0.667993, + 0.544519, + 0.371943, + 0.148988, + 0.001975, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.044071, + 0.268554, + 0.486129, + 0.641684, + 0.72169, + 0.72845, + 0.664792, + 0.549629, + 0.373723, + 0.152679, + 0.002422, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.042742, + 0.244106, + 0.452683, + 0.592429, + 0.659908, + 0.667497, + 0.625782, + 0.503856, + 0.327444, + 0.124567, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.074049, + 0.312187, + 0.525019, + 0.673272, + 0.740106, + 0.759662, + 0.699453, + 0.574768, + 0.39795, + 0.175855, + 0.006244, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.079866, + 0.315655, + 0.444964, + 0.652988, + 0.727222, + 0.727995, + 0.674817, + 0.563161, + 0.379039, + 0.164413, + 0.005614, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.065436, + 0.266827, + 0.461121, + 0.597509, + 0.680253, + 0.683872, + 0.626587, + 0.506131, + 0.336079, + 0.134546, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.077462, + 0.281816, + 0.477421, + 0.605007, + 0.6969, + 0.707003, + 0.585713, + 0.514137, + 0.344092, + 0.140595, + 0.002859, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.09086, + 0.310737, + 0.499569, + 0.450351, + 0.090902, + 0.092129, + 0.535872, + 0.144914, + 0.10601, + 0.018192, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.031061, + 0.178841, + 0.058363, + 0.322012, + 0.099641, + 0.215081, + 0.377066, + 0.065491, + 0.363583, + 0.158638, + 0.007151, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.126596, + 0.351016, + 0.508894, + 0.656818, + 0.729115, + 0.748269, + 0.696299, + 0.574509, + 0.356419, + 0.040467, + 0.010271, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.057839, + 0.185829, + 0.305539, + 0.397559, + 0.084201, + 0.388261, + 0.502019, + 0.062655, + 0.237244, + 0.077168, + 0.002677, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.131121, + 0.34705, + 0.168809, + 0.649756, + 0.620949, + 0.387096, + 0.297508, + 0.147099, + 0.144059, + 0.022144, + 0.000957, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.115675, + 0.117655, + 0.138028, + 0.287031, + 0.699036, + 0.699976, + 0.650197, + 0.5351, + 0.367484, + 0.142273, + 0.010436, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.019779, + 0.047548, + 0.105082, + 0.2606, + 0.108185, + 0.109295, + 0.099314, + 0.081763, + 0.148014, + 0.076518, + 0.002184, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002762, + 0.105006, + 0.040096, + 0.065851, + 0.267123, + 0.097494, + 0.099068, + 0.449283, + 0.081896, + 0.302403, + 0.062172, + 0.003999, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001329, + 0.055049, + 0.334889, + 0.538834, + 0.649407, + 0.747753, + 0.746849, + 0.70788, + 0.596598, + 0.413564, + 0.119584, + 0.017443, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007511, + 0.121516, + 0.375473, + 0.558274, + 0.578593, + 0.754519, + 0.324925, + 0.343152, + 0.587711, + 0.29803, + 0.214852, + 0.006656, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.009814, + 0.177718, + 0.384358, + 0.544646, + 0.652906, + 0.748051, + 0.714459, + 0.665866, + 0.600346, + 0.435514, + 0.231133, + 0.03933, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00844, + 0.032265, + 0.065104, + 0.074333, + 0.178745, + 0.102421, + 0.682715, + 0.286306, + 0.079264, + 0.053744, + 0.110118, + 0.01005, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002291, + 0.028948, + 0.059418, + 0.087497, + 0.106544, + 0.118611, + 0.119411, + 0.110894, + 0.093076, + 0.066827, + 0.122273, + 0.008096, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006478, + 0.031557, + 0.047576, + 0.072432, + 0.092314, + 0.513654, + 0.633444, + 0.229708, + 0.402844, + 0.371927, + 0.075832, + 0.014642, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006928, + 0.027407, + 0.379246, + 0.090211, + 0.577175, + 0.761747, + 0.46577, + 0.31667, + 0.517805, + 0.365166, + 0.021312, + 0.017029, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008741, + 0.028321, + 0.056864, + 0.084736, + 0.104187, + 0.116955, + 0.117503, + 0.106854, + 0.089186, + 0.062785, + 0.034091, + 0.013065, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011135, + 0.028403, + 0.055097, + 0.081924, + 0.101758, + 0.11251, + 0.114652, + 0.104729, + 0.086227, + 0.060188, + 0.032514, + 0.04799, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013561, + 0.031672, + 0.056471, + 0.083167, + 0.270742, + 0.115463, + 0.115745, + 0.105697, + 0.085863, + 0.060799, + 0.034139, + 0.018025, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013056, + 0.032219, + 0.061434, + 0.088316, + 0.087225, + 0.120133, + 0.120907, + 0.335188, + 0.091817, + 0.065374, + 0.02954, + 0.033614, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011956, + 0.04151, + 0.241932, + 0.199007, + 0.70481, + 0.833333, + 0.741209, + 0.771225, + 0.652838, + 0.489378, + 0.277037, + 0.067579, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017743, + 0.034273, + 0.062219, + 0.089059, + 0.229426, + 0.527705, + 0.742646, + 0.670853, + 0.467391, + 0.439114, + 0.235176, + 0.04382, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.043006, + 0.212791, + 0.418885, + 0.563683, + 0.673035, + 0.771321, + 0.770856, + 0.677946, + 0.600122, + 0.433723, + 0.233642, + 0.056998, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.046843, + 0.227282, + 0.415383, + 0.503709, + 0.420629, + 0.736205, + 0.735758, + 0.588777, + 0.485724, + 0.41985, + 0.195217, + 0.02911, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.050812, + 0.235418, + 0.446997, + 0.614418, + 0.690618, + 0.741817, + 0.741756, + 0.693202, + 0.621561, + 0.464748, + 0.259956, + 0.06653, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.051914, + 0.232462, + 0.439194, + 0.605965, + 0.716796, + 0.769521, + 0.731622, + 0.722468, + 0.609118, + 0.448169, + 0.247961, + 0.063315, + 6.8e-05, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.058239, + 0.249946, + 0.459515, + 0.620503, + 0.745691, + 0.793063, + 0.79457, + 0.1806, + 0.086966, + 0.0644, + 0.22584, + 0.049713, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.018872, + 0.035773, + 0.060951, + 0.084267, + 0.100687, + 0.109957, + 0.793795, + 0.739779, + 0.584073, + 0.42728, + 0.237231, + 0.066018, + 0.001115, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002413, + 0.061572, + 0.12479, + 0.413721, + 0.522229, + 0.752338, + 0.810942, + 0.810693, + 0.188268, + 0.090486, + 0.21763, + 0.244381, + 0.018176, + 7.1e-05, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000656, + 0.065684, + 0.257099, + 0.458795, + 0.628527, + 0.737883, + 0.791382, + 0.594085, + 0.731033, + 0.582984, + 0.439883, + 0.261869, + 0.069928, + 0.000776, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001685, + 0.062462, + 0.235891, + 0.279364, + 0.257909, + 0.104626, + 0.112757, + 0.321447, + 0.436331, + 0.286229, + 0.085598, + 0.07544, + 0.019987, + 0.001559, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002499, + 0.071639, + 0.044591, + 0.215761, + 0.076192, + 0.228832, + 0.641802, + 0.601588, + 0.526587, + 0.176784, + 0.071018, + 0.04471, + 0.020486, + 0.001508, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000436, + 0.024818, + 0.052119, + 0.314228, + 0.643682, + 0.73445, + 0.782676, + 0.776061, + 0.725608, + 0.630471, + 0.482115, + 0.051108, + 0.025113, + 0.006021, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004059, + 0.072729, + 0.21717, + 0.324478, + 0.610044, + 0.603248, + 0.696604, + 0.725535, + 0.273114, + 0.276138, + 0.074292, + 0.048001, + 0.022273, + 0.003676, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00337, + 0.032025, + 0.066601, + 0.077406, + 0.099754, + 0.115965, + 0.123999, + 0.12523, + 0.264781, + 0.09994, + 0.077326, + 0.088773, + 0.025031, + 0.004253, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008305, + 0.08158, + 0.27522, + 0.470191, + 0.623477, + 0.718391, + 0.780089, + 0.778407, + 0.724308, + 0.62043, + 0.469058, + 0.273526, + 0.082641, + 0.00904, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008413, + 0.057557, + 0.146378, + 0.069122, + 0.092253, + 0.108397, + 0.117732, + 0.116787, + 0.106882, + 0.091762, + 0.069396, + 0.044317, + 0.026789, + 0.009, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003042, + 0.019233, + 0.037936, + 0.0682, + 0.10653, + 0.121953, + 0.131047, + 0.13056, + 0.121399, + 0.104847, + 0.083144, + 0.057065, + 0.02935, + 0.006402, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007254, + 0.031043, + 0.202471, + 0.293896, + 0.650292, + 0.660284, + 0.322746, + 0.666715, + 0.203795, + 0.105468, + 0.082902, + 0.186579, + 0.077472, + 0.010028, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016225, + 0.09494, + 0.295953, + 0.493294, + 0.646773, + 0.747628, + 0.735894, + 0.776093, + 0.72316, + 0.636237, + 0.1494, + 0.234582, + 0.086053, + 0.012607, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010554, + 0.101405, + 0.319903, + 0.495571, + 0.657419, + 0.771884, + 0.814617, + 0.81306, + 0.759436, + 0.654251, + 0.502181, + 0.310044, + 0.095079, + 0.010147, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013555, + 0.051389, + 0.132435, + 0.086379, + 0.551078, + 0.502295, + 0.36769, + 0.185031, + 0.283075, + 0.309942, + 0.205477, + 0.103958, + 0.027505, + 0.009151, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015453, + 0.090485, + 0.259903, + 0.415555, + 0.596168, + 0.699221, + 0.397421, + 0.604303, + 0.560751, + 0.192361, + 0.441431, + 0.099732, + 0.074755, + 0.01465, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017871, + 0.094757, + 0.27108, + 0.33298, + 0.605415, + 0.534929, + 0.779662, + 0.58624, + 0.624643, + 0.555007, + 0.208163, + 0.13393, + 0.028896, + 0.012226, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013328, + 0.110179, + 0.321294, + 0.49112, + 0.653183, + 0.761074, + 0.804068, + 0.821731, + 0.757245, + 0.649795, + 0.50163, + 0.306735, + 0.097042, + 0.012772, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017955, + 0.11013, + 0.310387, + 0.499019, + 0.632301, + 0.735504, + 0.781807, + 0.791567, + 0.233135, + 0.631896, + 0.480741, + 0.293552, + 0.096527, + 0.016625, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000173, + 0.028535, + 0.110177, + 0.284409, + 0.416379, + 0.608367, + 0.69578, + 0.772673, + 0.760742, + 0.701627, + 0.61058, + 0.453574, + 0.275629, + 0.099178, + 0.025525, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.024907, + 0.101955, + 0.269086, + 0.434978, + 0.561098, + 0.679272, + 0.690076, + 0.723114, + 0.636172, + 0.578207, + 0.433188, + 0.256162, + 0.092022, + 0.023018, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000159, + 0.020085, + 0.112916, + 0.306502, + 0.491558, + 0.602333, + 0.745041, + 0.785175, + 0.776998, + 0.671707, + 0.5679, + 0.478548, + 0.291082, + 0.099064, + 0.018347, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000989, + 0.02194, + 0.117615, + 0.299834, + 0.456785, + 0.634241, + 0.281839, + 0.756008, + 0.709858, + 0.668967, + 0.590259, + 0.471829, + 0.29022, + 0.038397, + 0.014768, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000987, + 0.024735, + 0.117846, + 0.304793, + 0.488041, + 0.596599, + 0.680156, + 0.720013, + 0.764899, + 0.720057, + 0.60435, + 0.463863, + 0.282063, + 0.098847, + 0.023132, + 0.000377, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002329, + 0.030694, + 0.111558, + 0.279065, + 0.430536, + 0.55467, + 0.643412, + 0.683171, + 0.675595, + 0.670186, + 0.568199, + 0.421815, + 0.253228, + 0.095279, + 0.028208, + 0.001125, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002516, + 0.043986, + 0.039347, + 0.339781, + 0.53036, + 0.676706, + 0.782083, + 0.204397, + 0.833333, + 0.784162, + 0.676584, + 0.34967, + 0.062808, + 0.0623, + 0.016814, + 6.499999999999999e-05, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003246, + 0.038796, + 0.12998, + 0.334431, + 0.144461, + 0.217793, + 0.128963, + 0.136162, + 0.136793, + 0.126826, + 0.14204, + 0.0883, + 0.061623, + 0.035746, + 0.02016, + 0.000838, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003064, + 0.020745, + 0.137253, + 0.072745, + 0.098998, + 0.693704, + 0.776256, + 0.623563, + 0.768133, + 0.557978, + 0.600387, + 0.524426, + 0.327779, + 0.114177, + 0.01797, + 0.00079, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004652, + 0.024169, + 0.133891, + 0.333754, + 0.52071, + 0.630925, + 0.752266, + 0.81333, + 0.807249, + 0.750441, + 0.652923, + 0.496885, + 0.313572, + 0.112368, + 0.017177, + 0.003294, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003332, + 0.022255, + 0.047552, + 0.075214, + 0.082103, + 0.662802, + 0.315054, + 0.508585, + 0.232087, + 0.135912, + 0.585661, + 0.097569, + 0.072003, + 0.044598, + 0.020249, + 0.001221, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006154, + 0.020902, + 0.140433, + 0.359048, + 0.544571, + 0.693818, + 0.790555, + 0.823354, + 0.833333, + 0.772274, + 0.667864, + 0.52759, + 0.331111, + 0.046432, + 0.021561, + 0.003606, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006879, + 0.035123, + 0.050048, + 0.070305, + 0.095717, + 0.117535, + 0.249991, + 0.823399, + 0.826062, + 0.766742, + 0.661637, + 0.507218, + 0.319768, + 0.118924, + 0.032369, + 0.011262, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008091, + 0.034987, + 0.148706, + 0.276936, + 0.529023, + 0.321925, + 0.8028, + 0.799785, + 0.833333, + 0.804768, + 0.697895, + 0.533028, + 0.30229, + 0.121084, + 0.020535, + 0.005692, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013425, + 0.033085, + 0.096691, + 0.095131, + 0.479316, + 0.603848, + 0.724712, + 0.684214, + 0.536106, + 0.70993, + 0.552876, + 0.170879, + 0.271561, + 0.110909, + 0.025471, + 0.006962, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014575, + 0.043598, + 0.062227, + 0.248249, + 0.407181, + 0.191693, + 0.132749, + 0.278881, + 0.375412, + 0.130469, + 0.113737, + 0.143406, + 0.1611, + 0.04081, + 0.032791, + 0.007882, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013739, + 0.029824, + 0.046692, + 0.229801, + 0.097993, + 0.356457, + 0.570958, + 0.800672, + 0.791179, + 0.683786, + 0.602777, + 0.502407, + 0.30118, + 0.119185, + 0.02645, + 0.010244, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010951, + 0.022338, + 0.148873, + 0.344223, + 0.31585, + 0.127158, + 0.140073, + 0.147278, + 0.179986, + 0.77286, + 0.668034, + 0.496642, + 0.163667, + 0.129268, + 0.052349, + 0.008993, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014193, + 0.028748, + 0.14634, + 0.349961, + 0.531861, + 0.665004, + 0.754432, + 0.780922, + 0.791701, + 0.74009, + 0.640315, + 0.500308, + 0.31662, + 0.120776, + 0.029024, + 0.011525, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017193, + 0.033257, + 0.14316, + 0.329689, + 0.481124, + 0.635315, + 0.733107, + 0.757079, + 0.755919, + 0.702213, + 0.61612, + 0.467692, + 0.301817, + 0.12046, + 0.03043, + 0.013999, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013776, + 0.024209, + 0.149129, + 0.352898, + 0.528222, + 0.621303, + 0.752717, + 0.792595, + 0.784421, + 0.742504, + 0.640445, + 0.501664, + 0.323989, + 0.122482, + 0.022407, + 0.013648, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014777, + 0.027234, + 0.147632, + 0.356352, + 0.529588, + 0.661012, + 0.75093, + 0.790472, + 0.781773, + 0.633782, + 0.569169, + 0.442573, + 0.275219, + 0.130597, + 0.025432, + 0.01248, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.012735, + 0.023784, + 0.155529, + 0.365112, + 0.540748, + 0.633426, + 0.705557, + 0.735746, + 0.733182, + 0.762579, + 0.666967, + 0.520469, + 0.33939, + 0.127298, + 0.022148, + 0.011216, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01344, + 0.024521, + 0.155669, + 0.370203, + 0.555812, + 0.69211, + 0.790861, + 0.833333, + 0.827343, + 0.775051, + 0.665152, + 0.524846, + 0.337627, + 0.129499, + 0.023299, + 0.011699, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.012964, + 0.022587, + 0.157987, + 0.376031, + 0.549689, + 0.640706, + 0.764476, + 0.826093, + 0.812572, + 0.76076, + 0.666111, + 0.519723, + 0.342723, + 0.131316, + 0.021553, + 0.011256, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017998, + 0.031548, + 0.153689, + 0.357788, + 0.533217, + 0.624284, + 0.758563, + 0.782717, + 0.794446, + 0.677929, + 0.653454, + 0.176304, + 0.153986, + 0.123381, + 0.053889, + 0.012579, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.021222, + 0.031942, + 0.161002, + 0.362776, + 0.536822, + 0.626685, + 0.74945, + 0.793895, + 0.806336, + 0.66633, + 0.635977, + 0.106034, + 0.32041, + 0.103593, + 0.022795, + 0.009704, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016708, + 0.038806, + 0.160307, + 0.353036, + 0.267424, + 0.584814, + 0.667125, + 0.630734, + 0.745022, + 0.776635, + 0.313669, + 0.356525, + 0.343715, + 0.134995, + 0.025784, + 0.015664, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.020182, + 0.039645, + 0.083091, + 0.078319, + 0.268947, + 0.444846, + 0.618978, + 0.597378, + 0.379836, + 0.24557, + 0.304871, + 0.328367, + 0.288687, + 0.128884, + 0.045728, + 0.023382, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.018887, + 0.022372, + 0.161159, + 0.363608, + 0.525039, + 0.639247, + 0.1476, + 0.153397, + 0.385787, + 0.70253, + 0.599139, + 0.454701, + 0.350689, + 0.10226, + 0.033862, + 0.012826, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014415, + 0.032238, + 0.056781, + 0.298653, + 0.250113, + 0.128655, + 0.183182, + 0.498818, + 0.427488, + 0.681001, + 0.594774, + 0.104632, + 0.079575, + 0.053304, + 0.030062, + 0.01302, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015866, + 0.037799, + 0.063666, + 0.074135, + 0.187721, + 0.134227, + 0.14816, + 0.155487, + 0.500933, + 0.793568, + 0.690017, + 0.542552, + 0.360517, + 0.09148, + 0.03503, + 0.013614, + 0.003449, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000386, + 0.015889, + 0.031966, + 0.056864, + 0.083603, + 0.108171, + 0.608822, + 0.186636, + 0.648425, + 0.418312, + 0.504561, + 0.191023, + 0.329472, + 0.089378, + 0.053545, + 0.029747, + 0.014381, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000406, + 0.016534, + 0.03197, + 0.056487, + 0.082209, + 0.107804, + 0.128764, + 0.103722, + 0.447448, + 0.316442, + 0.141869, + 0.126397, + 0.104882, + 0.290818, + 0.065083, + 0.085127, + 0.02176, + 0.000968, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000744, + 0.026318, + 0.049117, + 0.117681, + 0.241623, + 0.504482, + 0.204597, + 0.365669, + 0.141858, + 0.451719, + 0.144386, + 0.12889, + 0.142013, + 0.158891, + 0.055939, + 0.031768, + 0.01515, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001152, + 0.019047, + 0.052126, + 0.153518, + 0.190355, + 0.151916, + 0.156859, + 0.141584, + 0.244285, + 0.148238, + 0.141738, + 0.125774, + 0.472753, + 0.078875, + 0.053071, + 0.030284, + 0.018128, + 0.001905, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002317, + 0.02009, + 0.029084, + 0.157921, + 0.363754, + 0.537074, + 0.665676, + 0.738965, + 0.789659, + 0.791184, + 0.733392, + 0.649236, + 0.520154, + 0.340405, + 0.14277, + 0.028247, + 0.019267, + 0.001441, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003567, + 0.027406, + 0.063351, + 0.114675, + 0.254699, + 0.198082, + 0.35159, + 0.133138, + 0.543068, + 0.459297, + 0.676307, + 0.553233, + 0.41294, + 0.299253, + 0.127619, + 0.032347, + 0.023609, + 0.000839, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004531, + 0.027114, + 0.025827, + 0.117021, + 0.357004, + 0.49796, + 0.613056, + 0.522812, + 0.698379, + 0.729751, + 0.754604, + 0.619562, + 0.483888, + 0.29812, + 0.131026, + 0.039504, + 0.03474, + 0.003276, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003699, + 0.030733, + 0.039722, + 0.143342, + 0.159846, + 0.168737, + 0.339324, + 0.65084, + 0.6674, + 0.72505, + 0.692504, + 0.604546, + 0.465, + 0.315546, + 0.120928, + 0.027999, + 0.01762, + 0.001283, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003047, + 0.016069, + 0.036352, + 0.076381, + 0.109222, + 0.109003, + 0.127441, + 0.140932, + 0.149176, + 0.148832, + 0.181698, + 0.163632, + 0.136781, + 0.105726, + 0.057679, + 0.034138, + 0.014439, + 0.000907, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002139, + 0.016106, + 0.037035, + 0.168617, + 0.086933, + 0.111275, + 0.130247, + 0.144001, + 0.151081, + 0.151045, + 0.142933, + 0.128829, + 0.266723, + 0.084293, + 0.058894, + 0.035098, + 0.01873, + 0.003029, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004604, + 0.022381, + 0.034418, + 0.074114, + 0.107682, + 0.109074, + 0.128405, + 0.648251, + 0.583861, + 0.646337, + 0.345015, + 0.12586, + 0.105905, + 0.080892, + 0.056328, + 0.032754, + 0.016307, + 0.001796, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005971, + 0.029634, + 0.061314, + 0.140101, + 0.300748, + 0.473757, + 0.575386, + 0.664769, + 0.702488, + 0.78048, + 0.722924, + 0.62498, + 0.488223, + 0.316466, + 0.150433, + 0.051067, + 0.034628, + 0.006305, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005932, + 0.024351, + 0.039982, + 0.145368, + 0.282544, + 0.309313, + 0.478826, + 0.659353, + 0.786648, + 0.498067, + 0.387678, + 0.599755, + 0.435118, + 0.288893, + 0.146497, + 0.045035, + 0.024653, + 0.007125, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004942, + 0.024513, + 0.050151, + 0.157947, + 0.34554, + 0.478751, + 0.587691, + 0.688413, + 0.593148, + 0.726364, + 0.59378, + 0.622132, + 0.226406, + 0.250551, + 0.157084, + 0.026401, + 0.017562, + 0.004186, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006297, + 0.024466, + 0.061392, + 0.09166, + 0.252586, + 0.294565, + 0.532256, + 0.748429, + 0.712586, + 0.71164, + 0.725522, + 0.594331, + 0.487294, + 0.341943, + 0.15507, + 0.034035, + 0.022038, + 0.005697, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006004, + 0.023083, + 0.034295, + 0.15739, + 0.350433, + 0.505084, + 0.633258, + 0.710055, + 0.76635, + 0.778258, + 0.737629, + 0.63723, + 0.510641, + 0.341403, + 0.152227, + 0.03367, + 0.021989, + 0.00574, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005689, + 0.019843, + 0.025559, + 0.154891, + 0.359354, + 0.406086, + 0.659401, + 0.766149, + 0.808458, + 0.768625, + 0.736665, + 0.64584, + 0.402071, + 0.347544, + 0.167008, + 0.040843, + 0.023126, + 0.006631, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005277, + 0.021938, + 0.073563, + 0.056469, + 0.196109, + 0.299993, + 0.306043, + 0.693462, + 0.722645, + 0.550172, + 0.650139, + 0.589657, + 0.424275, + 0.24024, + 0.141495, + 0.067452, + 0.02173, + 0.005298, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008773, + 0.032797, + 0.045914, + 0.128002, + 0.28652, + 0.461556, + 0.552153, + 0.559504, + 0.569611, + 0.495777, + 0.745023, + 0.658166, + 0.516599, + 0.350646, + 0.15913, + 0.039511, + 0.026728, + 0.008617, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008924, + 0.018567, + 0.030234, + 0.156229, + 0.229012, + 0.46578, + 0.656946, + 0.735438, + 0.747363, + 0.653958, + 0.708089, + 0.60523, + 0.498873, + 0.359464, + 0.156462, + 0.026893, + 0.018161, + 0.007115, + 0, + 0, + 0, + 0, + 0, + 0, + 0.009176, + 0.028509, + 0.03464, + 0.153907, + 0.257091, + 0.473566, + 0.627262, + 0.717642, + 0.764125, + 0.75425, + 0.721839, + 0.639684, + 0.515522, + 0.346344, + 0.155293, + 0.034079, + 0.02281, + 0.007512, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005494, + 0.017162, + 0.026453, + 0.154053, + 0.358261, + 0.526465, + 0.662752, + 0.755882, + 0.791872, + 0.783159, + 0.744939, + 0.650065, + 0.525712, + 0.360243, + 0.158942, + 0.026655, + 0.016728, + 0.006227, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008566, + 0.026471, + 0.038242, + 0.155303, + 0.328556, + 0.489327, + 0.564908, + 0.637069, + 0.687676, + 0.7447, + 0.728895, + 0.579664, + 0.482689, + 0.324752, + 0.156315, + 0.051971, + 0.031137, + 0.008421, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011731, + 0.023409, + 0.027429, + 0.143051, + 0.307469, + 0.482622, + 0.633327, + 0.74568, + 0.747857, + 0.774112, + 0.646494, + 0.611821, + 0.504904, + 0.32613, + 0.166156, + 0.038059, + 0.02134, + 0.010509, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007428, + 0.028886, + 0.069931, + 0.129271, + 0.24725, + 0.472415, + 0.595138, + 0.587632, + 0.714664, + 0.31184, + 0.420491, + 0.607797, + 0.398588, + 0.192644, + 0.054602, + 0.04679, + 0.024705, + 0.009525, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007789, + 0.026529, + 0.071756, + 0.122094, + 0.318598, + 0.452161, + 0.532808, + 0.641984, + 0.726504, + 0.726953, + 0.689352, + 0.617454, + 0.49805, + 0.327573, + 0.158043, + 0.041333, + 0.027383, + 0.009125, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007449, + 0.026149, + 0.042781, + 0.071036, + 0.103601, + 0.495088, + 0.617384, + 0.716404, + 0.749204, + 0.751823, + 0.728898, + 0.630124, + 0.499896, + 0.337639, + 0.156616, + 0.038148, + 0.032435, + 0.008982, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005749, + 0.019625, + 0.027897, + 0.145929, + 0.295528, + 0.490238, + 0.604031, + 0.702153, + 0.737681, + 0.737164, + 0.677958, + 0.639184, + 0.424382, + 0.350012, + 0.159465, + 0.054205, + 0.021563, + 0.007996, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00463, + 0.026289, + 0.067393, + 0.146609, + 0.223872, + 0.20273, + 0.429563, + 0.716402, + 0.776443, + 0.701226, + 0.654993, + 0.500694, + 0.428176, + 0.353111, + 0.164244, + 0.028099, + 0.017798, + 0.00643, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005079, + 0.02234, + 0.047541, + 0.133444, + 0.285549, + 0.478759, + 0.435842, + 0.401033, + 0.413451, + 0.513443, + 0.439704, + 0.518196, + 0.192967, + 0.185603, + 0.167542, + 0.037059, + 0.041733, + 0.008593, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003542, + 0.038483, + 0.022858, + 0.137362, + 0.320568, + 0.486634, + 0.417822, + 0.743154, + 0.417391, + 0.428285, + 0.66526, + 0.644163, + 0.547086, + 0.340839, + 0.130014, + 0.025128, + 0.015106, + 0.005904, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006685, + 0.029044, + 0.058431, + 0.072481, + 0.320084, + 0.460964, + 0.572645, + 0.61729, + 0.684535, + 0.702578, + 0.681111, + 0.583454, + 0.476898, + 0.320984, + 0.161951, + 0.058876, + 0.030046, + 0.009978, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006077, + 0.041038, + 0.037723, + 0.152703, + 0.32584, + 0.483094, + 0.608358, + 0.483236, + 0.650101, + 0.626053, + 0.573708, + 0.203828, + 0.434407, + 0.333179, + 0.163923, + 0.032888, + 0.032032, + 0.008014, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005838, + 0.02998, + 0.049018, + 0.138238, + 0.210337, + 0.437229, + 0.530068, + 0.137773, + 0.146664, + 0.519387, + 0.32783, + 0.17684, + 0.174927, + 0.117263, + 0.081112, + 0.052404, + 0.032138, + 0.009312, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00472, + 0.022658, + 0.033357, + 0.145732, + 0.235983, + 0.387062, + 0.428528, + 0.71736, + 0.711481, + 0.733913, + 0.411216, + 0.410489, + 0.264238, + 0.262874, + 0.05457, + 0.063309, + 0.021828, + 0.006901, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002874, + 0.022855, + 0.048219, + 0.153615, + 0.326289, + 0.462256, + 0.573798, + 0.742092, + 0.758825, + 0.773675, + 0.69655, + 0.529598, + 0.487943, + 0.271818, + 0.163915, + 0.024565, + 0.024332, + 0.011129, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004193, + 0.020525, + 0.048404, + 0.105563, + 0.10445, + 0.260363, + 0.632816, + 0.605138, + 0.499668, + 0.56178, + 0.376413, + 0.366506, + 0.158473, + 0.101298, + 0.140265, + 0.065373, + 0.037801, + 0.007128, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000979, + 0.016635, + 0.037441, + 0.061792, + 0.087996, + 0.111426, + 0.131246, + 0.64087, + 0.603441, + 0.677146, + 0.713023, + 0.360672, + 0.114393, + 0.091071, + 0.102848, + 0.040554, + 0.038045, + 0.005727, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000645, + 0.018016, + 0.03946, + 0.064257, + 0.090579, + 0.364607, + 0.134354, + 0.148457, + 0.156032, + 0.157211, + 0.389781, + 0.307359, + 0.365972, + 0.22999, + 0.162661, + 0.075075, + 0.041339, + 0.006885, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002498, + 0.021318, + 0.03692, + 0.04216, + 0.087472, + 0.111208, + 0.510899, + 0.144401, + 0.478644, + 0.698201, + 0.657259, + 0.499991, + 0.302847, + 0.090962, + 0.149673, + 0.039739, + 0.019164, + 0.003022, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002321, + 0.036991, + 0.053852, + 0.109322, + 0.108745, + 0.07522, + 0.370187, + 0.569724, + 0.709397, + 0.676704, + 0.419681, + 0.613744, + 0.465283, + 0.353961, + 0.160879, + 0.02448, + 0.016035, + 0.003806, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003199, + 0.026973, + 0.032234, + 0.138881, + 0.308338, + 0.509075, + 0.633268, + 0.704233, + 0.77173, + 0.77217, + 0.724596, + 0.609879, + 0.500687, + 0.363909, + 0.161549, + 0.049898, + 0.016585, + 0.00468, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000222, + 0.026811, + 0.023293, + 0.14043, + 0.330951, + 0.504335, + 0.641794, + 0.673063, + 0.765555, + 0.783267, + 0.728239, + 0.649056, + 0.522548, + 0.338196, + 0.162869, + 0.025166, + 0.025881, + 0.003292, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00155, + 0.015303, + 0.025351, + 0.119406, + 0.334129, + 0.468321, + 0.638871, + 0.718605, + 0.772256, + 0.766194, + 0.748335, + 0.650585, + 0.527369, + 0.363486, + 0.163502, + 0.023874, + 0.016766, + 0.003398, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001361, + 0.019436, + 0.028874, + 0.133426, + 0.325257, + 0.502282, + 0.64582, + 0.745989, + 0.76884, + 0.795634, + 0.757385, + 0.66385, + 0.526913, + 0.359956, + 0.167359, + 0.035641, + 0.032174, + 0.004144, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001089, + 0.016978, + 0.038122, + 0.137809, + 0.302508, + 0.456079, + 0.466552, + 0.453394, + 0.266357, + 0.146428, + 0.167658, + 0.375846, + 0.106752, + 0.168532, + 0.172288, + 0.083025, + 0.026247, + 0.00263, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001011, + 0.018873, + 0.022145, + 0.1302, + 0.20617, + 0.465029, + 0.597956, + 0.68554, + 0.761079, + 0.743999, + 0.695332, + 0.611406, + 0.506014, + 0.27331, + 0.062912, + 0.038536, + 0.022598, + 0.001785, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00099, + 0.007327, + 0.042302, + 0.150628, + 0.296353, + 0.46823, + 0.585618, + 0.690442, + 0.733703, + 0.731781, + 0.642393, + 0.568023, + 0.487031, + 0.321277, + 0.151253, + 0.048162, + 0.038774, + 0.002208, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000604, + 0.026304, + 0.075144, + 0.085296, + 0.072342, + 0.292323, + 0.336981, + 0.387919, + 0.138176, + 0.702427, + 0.684362, + 0.383496, + 0.254967, + 0.076483, + 0.131199, + 0.034048, + 0.020366, + 0.003063, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.024756, + 0.060562, + 0.121257, + 0.304533, + 0.476507, + 0.57249, + 0.675177, + 0.749491, + 0.737378, + 0.688534, + 0.419728, + 0.224654, + 0.296686, + 0.139128, + 0.068702, + 0.033732, + 0.001625, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000777, + 0.030484, + 0.036359, + 0.087462, + 0.304359, + 0.462881, + 0.590363, + 0.648438, + 0.701119, + 0.768415, + 0.733278, + 0.646288, + 0.512802, + 0.349757, + 0.154241, + 0.035703, + 0.033647, + 0.002137, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.021708, + 0.037123, + 0.118393, + 0.276452, + 0.431513, + 0.540953, + 0.583274, + 0.695185, + 0.719129, + 0.672755, + 0.59966, + 0.47415, + 0.318661, + 0.146917, + 0.041294, + 0.023898, + 0.002007, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.022172, + 0.042965, + 0.087779, + 0.165703, + 0.182494, + 0.16825, + 0.193879, + 0.722641, + 0.68518, + 0.626671, + 0.610666, + 0.488594, + 0.326376, + 0.104085, + 0.047143, + 0.025541, + 0.002022, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.023057, + 0.055206, + 0.067053, + 0.285683, + 0.453863, + 0.525608, + 0.163733, + 0.380163, + 0.679159, + 0.671456, + 0.444257, + 0.253386, + 0.081776, + 0.056099, + 0.063882, + 0.017529, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.026921, + 0.062578, + 0.094837, + 0.115935, + 0.365425, + 0.302369, + 0.306399, + 0.304961, + 0.358549, + 0.343113, + 0.324407, + 0.185261, + 0.178703, + 0.145389, + 0.040046, + 0.018614, + 7e-06, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014183, + 0.024561, + 0.084545, + 0.126092, + 0.442879, + 0.419756, + 0.700801, + 0.247027, + 0.146104, + 0.588908, + 0.486137, + 0.496738, + 0.342331, + 0.105577, + 0.031758, + 0.012045, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013815, + 0.028178, + 0.051428, + 0.076556, + 0.293402, + 0.122236, + 0.327034, + 0.143482, + 0.145313, + 0.139606, + 0.125816, + 0.335331, + 0.177254, + 0.117351, + 0.050627, + 0.012073, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011241, + 0.035549, + 0.114125, + 0.275531, + 0.39002, + 0.328366, + 0.132383, + 0.13937, + 0.499649, + 0.556939, + 0.120012, + 0.100261, + 0.077089, + 0.051268, + 0.028531, + 0.013094, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.019633, + 0.047573, + 0.108917, + 0.262295, + 0.384701, + 0.558036, + 0.557038, + 0.632208, + 0.694623, + 0.660091, + 0.641286, + 0.459042, + 0.2946, + 0.128531, + 0.046484, + 0.018498, + 6e-06, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.023097, + 0.037615, + 0.08453, + 0.105949, + 0.465377, + 0.594983, + 0.698611, + 0.45761, + 0.207392, + 0.322397, + 0.328201, + 0.220804, + 0.153015, + 0.097036, + 0.033844, + 0.01894, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016119, + 0.035638, + 0.112652, + 0.266136, + 0.309808, + 0.329692, + 0.221636, + 0.237517, + 0.466266, + 0.275178, + 0.19705, + 0.216207, + 0.220897, + 0.137808, + 0.036586, + 0.020023, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010245, + 0.019573, + 0.036987, + 0.054185, + 0.070198, + 0.101447, + 0.494469, + 0.723782, + 0.658772, + 0.685707, + 0.529492, + 0.532811, + 0.358332, + 0.154638, + 0.042485, + 0.026858, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.021285, + 0.05107, + 0.115074, + 0.288516, + 0.424509, + 0.562912, + 0.641469, + 0.757866, + 0.68587, + 0.711926, + 0.562121, + 0.486892, + 0.323519, + 0.137371, + 0.04564, + 0.019281, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014464, + 0.037825, + 0.104888, + 0.240626, + 0.463191, + 0.583614, + 0.703572, + 0.664494, + 0.734046, + 0.719561, + 0.625947, + 0.49966, + 0.303918, + 0.139204, + 0.021356, + 0.012753, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007005, + 0.035735, + 0.055073, + 0.07833, + 0.102716, + 0.12261, + 0.711143, + 0.716412, + 0.739994, + 0.697283, + 0.363256, + 0.152518, + 0.147497, + 0.13497, + 0.024073, + 0.016125, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005592, + 0.017591, + 0.107059, + 0.303461, + 0.377985, + 0.612615, + 0.704199, + 0.745895, + 0.764419, + 0.676473, + 0.609804, + 0.465014, + 0.1672, + 0.110188, + 0.052447, + 0.014467, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010776, + 0.036397, + 0.025293, + 0.040042, + 0.288315, + 0.519298, + 0.601625, + 0.360404, + 0.129451, + 0.122978, + 0.108229, + 0.088446, + 0.064498, + 0.040531, + 0.027111, + 0.010998, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008527, + 0.027947, + 0.086222, + 0.130602, + 0.193032, + 0.429635, + 0.301062, + 0.411237, + 0.258628, + 0.541273, + 0.619677, + 0.224476, + 0.159642, + 0.092259, + 0.042606, + 0.015721, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004933, + 0.023129, + 0.112555, + 0.167295, + 0.137777, + 0.157287, + 0.382854, + 0.387079, + 0.588397, + 0.643616, + 0.540835, + 0.424396, + 0.292668, + 0.063713, + 0.030737, + 0.009346, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006726, + 0.034476, + 0.11755, + 0.17723, + 0.215339, + 0.279723, + 0.459145, + 0.524816, + 0.386907, + 0.694487, + 0.569775, + 0.468533, + 0.320739, + 0.127708, + 0.043607, + 0.013631, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004099, + 0.025211, + 0.102331, + 0.240039, + 0.434535, + 0.591978, + 0.563867, + 0.693312, + 0.693179, + 0.617049, + 0.440591, + 0.414262, + 0.244296, + 0.041539, + 0.030801, + 0.008965, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002022, + 0.014166, + 0.095007, + 0.269838, + 0.449189, + 0.553007, + 0.66101, + 0.733428, + 0.729579, + 0.692456, + 0.600192, + 0.480764, + 0.31548, + 0.131077, + 0.017739, + 0.00571, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.019675, + 0.042757, + 0.068804, + 0.093091, + 0.113907, + 0.127641, + 0.136302, + 0.750268, + 0.129359, + 0.115455, + 0.095669, + 0.072286, + 0.085487, + 0.040704, + 0.002757, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005372, + 0.026535, + 0.082448, + 0.103252, + 0.322859, + 0.506187, + 0.654797, + 0.698261, + 0.672273, + 0.67965, + 0.530865, + 0.451503, + 0.280958, + 0.131941, + 0.024235, + 0.013332, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.019839, + 0.0616, + 0.239946, + 0.40647, + 0.113045, + 0.127625, + 0.133859, + 0.612029, + 0.127657, + 0.114885, + 0.096024, + 0.072851, + 0.047019, + 0.023138, + 0.001139, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000848, + 0.032258, + 0.074484, + 0.200724, + 0.238984, + 0.27729, + 0.366295, + 0.546403, + 0.509164, + 0.495234, + 0.333845, + 0.142748, + 0.17899, + 0.108854, + 0.033391, + 0.004278, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015716, + 0.025525, + 0.042732, + 0.058709, + 0.07241, + 0.082137, + 0.107102, + 0.488229, + 0.272826, + 0.111152, + 0.091269, + 0.066826, + 0.041809, + 0.017046, + 0.000177, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.009035, + 0.024795, + 0.041814, + 0.057435, + 0.086828, + 0.23894, + 0.511181, + 0.656314, + 0.677225, + 0.591285, + 0.438808, + 0.27514, + 0.114909, + 0.022146, + 0.001044, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013824, + 0.095714, + 0.296137, + 0.475019, + 0.599864, + 0.690421, + 0.714693, + 0.756746, + 0.704988, + 0.598674, + 0.496726, + 0.315995, + 0.120816, + 0.015138, + 0.000698, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011964, + 0.093598, + 0.285044, + 0.451807, + 0.574889, + 0.65869, + 0.752194, + 0.74667, + 0.715719, + 0.626154, + 0.488181, + 0.317839, + 0.117382, + 0.01257, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01758, + 0.091803, + 0.095299, + 0.453439, + 0.585409, + 0.653608, + 0.753632, + 0.743602, + 0.650712, + 0.486754, + 0.356145, + 0.235869, + 0.078823, + 0.033481, + 0.0005, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000777, + 0.032691, + 0.05039, + 0.084757, + 0.149791, + 0.107245, + 0.220748, + 0.364212, + 0.262862, + 0.681788, + 0.58833, + 0.470694, + 0.295502, + 0.106487, + 0.012581, + 0.000739, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.018471, + 0.060439, + 0.162643, + 0.226941, + 0.253829, + 0.46605, + 0.727358, + 0.703792, + 0.653516, + 0.543101, + 0.425296, + 0.225277, + 0.104443, + 0.022462, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.012295, + 0.096137, + 0.250497, + 0.406373, + 0.105749, + 0.33376, + 0.675276, + 0.696085, + 0.674719, + 0.539048, + 0.441973, + 0.162977, + 0.048105, + 0.018558, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015842, + 0.045308, + 0.078963, + 0.111236, + 0.134952, + 0.155135, + 0.164506, + 0.161778, + 0.587324, + 0.137447, + 0.421933, + 0.285101, + 0.104186, + 0.014041, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016325, + 0.088459, + 0.126053, + 0.39675, + 0.231057, + 0.547991, + 0.225075, + 0.619872, + 0.183767, + 0.22265, + 0.151728, + 0.085445, + 0.034832, + 0.0177, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017519, + 0.038449, + 0.075874, + 0.240216, + 0.352144, + 0.63156, + 0.386203, + 0.423578, + 0.535039, + 0.515768, + 0.368736, + 0.245813, + 0.088659, + 0.019204, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005934, + 0.022658, + 0.039903, + 0.056103, + 0.069273, + 0.079729, + 0.08441, + 0.633317, + 0.283016, + 0.106859, + 0.086358, + 0.106128, + 0.03547, + 0.025639, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01803, + 0.084808, + 0.234841, + 0.388793, + 0.542138, + 0.666366, + 0.767971, + 0.70547, + 0.652512, + 0.552262, + 0.414162, + 0.228692, + 0.078206, + 0.013026, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.009594, + 0.085263, + 0.26328, + 0.473174, + 0.579747, + 0.402679, + 0.196232, + 0.633409, + 0.24167, + 0.241944, + 0.383899, + 0.057904, + 0.088303, + 0.021553, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.018752, + 0.053471, + 0.108829, + 0.311604, + 0.247841, + 0.288227, + 0.696496, + 0.617283, + 0.56792, + 0.418357, + 0.141534, + 0.112616, + 0.044043, + 0.022972, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013143, + 0.029088, + 0.055303, + 0.078933, + 0.30096, + 0.178696, + 0.123316, + 0.750813, + 0.669446, + 0.461026, + 0.392269, + 0.024094, + 0.059667, + 0.006468, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.009074, + 0.074645, + 0.26563, + 0.444834, + 0.588176, + 0.500083, + 0.550118, + 0.750177, + 0.550253, + 0.210376, + 0.111539, + 0.122657, + 0.05583, + 0.015692, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00556, + 0.079854, + 0.047422, + 0.144474, + 0.638864, + 0.358708, + 0.741205, + 0.647146, + 0.663614, + 0.219937, + 0.082019, + 0.038558, + 0.05556, + 0.005906, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.005654, + 0.068293, + 0.175544, + 0.277411, + 0.606082, + 0.627947, + 0.781107, + 0.636611, + 0.71349, + 0.280082, + 0.269902, + 0.116381, + 0.034115, + 0.01151, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006871, + 0.055369, + 0.170251, + 0.377371, + 0.563232, + 0.445119, + 0.705566, + 0.398475, + 0.114031, + 0.099334, + 0.07924, + 0.054587, + 0.063908, + 0.010502, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002051, + 0.019912, + 0.043, + 0.06705, + 0.535259, + 0.629358, + 0.733847, + 0.745866, + 0.682017, + 0.363796, + 0.27278, + 0.121051, + 0.056101, + 0.003161, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004887, + 0.038926, + 0.165745, + 0.265608, + 0.440227, + 0.463155, + 0.699891, + 0.401582, + 0.719871, + 0.581072, + 0.425137, + 0.211571, + 0.074761, + 0.006522, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002135, + 0.069218, + 0.250933, + 0.435821, + 0.557967, + 0.646315, + 0.691095, + 0.688633, + 0.639655, + 0.570597, + 0.422778, + 0.242323, + 0.064365, + 0.001407, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001028, + 0.07443, + 0.26982, + 0.449297, + 0.584009, + 0.668629, + 0.708907, + 0.746595, + 0.65825, + 0.593299, + 0.453098, + 0.262079, + 0.066238, + 0.000452, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000892, + 0.068914, + 0.25539, + 0.443759, + 0.584441, + 0.65153, + 0.739964, + 0.729477, + 0.675838, + 0.579485, + 0.428285, + 0.23947, + 0.059641, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.073039, + 0.282209, + 0.472699, + 0.617434, + 0.706524, + 0.772803, + 0.706114, + 0.706745, + 0.598601, + 0.451668, + 0.253526, + 0.06119, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000108, + 0.066973, + 0.251927, + 0.444843, + 0.596009, + 0.697341, + 0.747003, + 0.747529, + 0.692806, + 0.574142, + 0.419164, + 0.232235, + 0.055628, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.070223, + 0.271714, + 0.456531, + 0.624489, + 0.723945, + 0.722835, + 0.762464, + 0.722333, + 0.609081, + 0.456352, + 0.258653, + 0.056552, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.066423, + 0.265967, + 0.459095, + 0.603913, + 0.704995, + 0.713393, + 0.709093, + 0.699133, + 0.592912, + 0.436768, + 0.2369, + 0.051695, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.065419, + 0.25957, + 0.456702, + 0.602962, + 0.716607, + 0.749766, + 0.766839, + 0.70292, + 0.584101, + 0.434602, + 0.23428, + 0.048306, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.063143, + 0.261081, + 0.458474, + 0.577226, + 0.702943, + 0.703044, + 0.704307, + 0.699684, + 0.590966, + 0.433503, + 0.227352, + 0.045673, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.065322, + 0.27086, + 0.473857, + 0.634652, + 0.751966, + 0.80722, + 0.798141, + 0.722383, + 0.614748, + 0.448015, + 0.238668, + 0.045032, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.062899, + 0.266352, + 0.45493, + 0.626388, + 0.731098, + 0.790796, + 0.788597, + 0.678805, + 0.601013, + 0.427763, + 0.230083, + 0.040792, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.061062, + 0.259607, + 0.455258, + 0.614469, + 0.721249, + 0.760646, + 0.70213, + 0.652128, + 0.582134, + 0.429163, + 0.223644, + 0.038198, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.056024, + 0.248325, + 0.430162, + 0.24645, + 0.097322, + 0.106021, + 0.104309, + 0.095901, + 0.245463, + 0.056054, + 0.128395, + 0.030865, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.042086, + 0.236088, + 0.335115, + 0.206873, + 0.332311, + 0.386063, + 0.535944, + 0.56964, + 0.416323, + 0.405487, + 0.200607, + 0.018164, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00499, + 0.020795, + 0.037111, + 0.579872, + 0.611329, + 0.51251, + 0.690166, + 0.477642, + 0.169713, + 0.350023, + 0.104852, + 0.02713, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.050268, + 0.149137, + 0.331572, + 0.453332, + 0.634748, + 0.44135, + 0.658844, + 0.442244, + 0.412, + 0.426845, + 0.14951, + 0.017631, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016526, + 0.048343, + 0.263622, + 0.384194, + 0.557227, + 0.677462, + 0.373027, + 0.528388, + 0.571075, + 0.349327, + 0.191828, + 0.008982, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008156, + 0.16151, + 0.06099, + 0.512167, + 0.6071, + 0.63195, + 0.657937, + 0.675261, + 0.578665, + 0.268234, + 0.148537, + 0.007806, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.025943, + 0.02332, + 0.18164, + 0.612445, + 0.69922, + 0.757307, + 0.789434, + 0.709357, + 0.415179, + 0.427397, + 0.205093, + 0.013403, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.012783, + 0.033706, + 0.077954, + 0.496634, + 0.301914, + 0.091881, + 0.09136, + 0.081405, + 0.063428, + 0.168935, + 0.028316, + 0.002486, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.037847, + 0.078622, + 0.206772, + 0.417909, + 0.463643, + 0.676167, + 0.733461, + 0.611713, + 0.55548, + 0.377695, + 0.071463, + 0.009161, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.036448, + 0.250892, + 0.479587, + 0.639062, + 0.722027, + 0.777181, + 0.770847, + 0.668958, + 0.54479, + 0.250626, + 0.115376, + 0.007828, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01739, + 0.137023, + 0.218386, + 0.218379, + 0.53252, + 0.625058, + 0.681643, + 0.641115, + 0.528151, + 0.386951, + 0.167926, + 0.007161, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006232, + 0.153486, + 0.317854, + 0.346958, + 0.501739, + 0.671357, + 0.637769, + 0.610799, + 0.240682, + 0.323742, + 0.090165, + 0.001423, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01126, + 0.170681, + 0.238283, + 0.605597, + 0.720883, + 0.773325, + 0.687455, + 0.621437, + 0.53652, + 0.377066, + 0.14059, + 0.00381, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.018835, + 0.102514, + 0.187571, + 0.13966, + 0.265319, + 0.51561, + 0.490887, + 0.193786, + 0.374928, + 0.357367, + 0.046117, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5.9e-05, + 0.019484, + 0.043134, + 0.223874, + 0.07933, + 0.08639, + 0.543543, + 0.046999, + 0.045283, + 0.107748, + 0.085042, + 0.000728, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.01436, + 0.21427, + 0.428842, + 0.532069, + 0.698438, + 0.743286, + 0.733036, + 0.662527, + 0.529603, + 0.35407, + 0.134041, + 0.001152, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015014, + 0.214804, + 0.358071, + 0.527145, + 0.699029, + 0.746151, + 0.554604, + 0.656836, + 0.530169, + 0.353999, + 0.05848, + 5.6e-05, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000113, + 0.032545, + 0.062976, + 0.088091, + 0.105292, + 0.113525, + 0.111898, + 0.100081, + 0.07964, + 0.052049, + 0.01979, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.022449, + 0.051036, + 0.076268, + 0.094523, + 0.102168, + 0.10054, + 0.08851, + 0.067082, + 0.040719, + 0.011769, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008878, + 0.204114, + 0.035992, + 0.340076, + 0.596584, + 0.754766, + 0.734234, + 0.660111, + 0.535318, + 0.341887, + 0.041918, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000539, + 0.146217, + 0.20828, + 0.488604, + 0.565531, + 0.100181, + 0.098013, + 0.085427, + 0.065499, + 0.03786, + 0.009408, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002847, + 0.162279, + 0.302993, + 0.533138, + 0.112135, + 0.197961, + 0.04892, + 0.540006, + 0.406996, + 0.187102, + 0.006635, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000249, + 0.096894, + 0.376028, + 0.536244, + 0.649655, + 0.495982, + 0.084651, + 0.072643, + 0.245223, + 0.104414, + 0.006006, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002556, + 0.157883, + 0.383414, + 0.564112, + 0.090843, + 0.098812, + 0.097285, + 0.084803, + 0.168807, + 0.311173, + 0.077993, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.016961, + 0.117476, + 0.114878, + 0.09569, + 0.103312, + 0.156557, + 0.376773, + 0.511511, + 0.294684, + 0.055266, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001156, + 0.144318, + 0.37813, + 0.573952, + 0.69317, + 0.738629, + 0.722155, + 0.63523, + 0.483363, + 0.306578, + 0.061702, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.122722, + 0.348904, + 0.541694, + 0.667835, + 0.714712, + 0.69821, + 0.420949, + 0.415198, + 0.279235, + 0.000562, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.121088, + 0.348765, + 0.542172, + 0.676241, + 0.733644, + 0.710409, + 0.499542, + 0.406887, + 0.18113, + 0.012167, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.008644, + 0.033015, + 0.056747, + 0.074143, + 0.082331, + 0.079611, + 0.067505, + 0.046477, + 0.021246, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.031661, + 0.122712, + 0.069665, + 0.659451, + 0.45266, + 0.463007, + 0.078647, + 0.057395, + 0.029, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.090609, + 0.316131, + 0.511685, + 0.63977, + 0.684475, + 0.695598, + 0.603307, + 0.464356, + 0.267755, + 0.031255, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.069695, + 0.283825, + 0.471648, + 0.606063, + 0.667825, + 0.418371, + 0.397196, + 0.041484, + 0.02424, + 0.00196, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.009218, + 0.036452, + 0.0607, + 0.078002, + 0.085208, + 0.082452, + 0.269742, + 0.048771, + 0.068421, + 0.028126, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003603, + 0.034912, + 0.060647, + 0.076888, + 0.085397, + 0.050441, + 0.069716, + 0.028405, + 0.02006, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001816, + 0.024543, + 0.028111, + 0.063243, + 0.070399, + 0.068068, + 0.055581, + 0.035445, + 0.012309, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000986, + 0.025647, + 0.048763, + 0.066022, + 0.073344, + 0.070955, + 0.057731, + 0.037502, + 0.012783, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001364, + 0.031637, + 0.056201, + 0.072917, + 0.080928, + 0.669196, + 0.583594, + 0.435594, + 0.225234, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.028844, + 0.237379, + 0.421993, + 0.552598, + 0.608669, + 0.660483, + 0.566784, + 0.418679, + 0.203473, + 0.013941, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.02659, + 0.2305, + 0.417971, + 0.545181, + 0.602832, + 0.672894, + 0.582202, + 0.428164, + 0.207328, + 0.017703, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.019468, + 0.224699, + 0.410041, + 0.519928, + 0.597321, + 0.634218, + 0.585682, + 0.434434, + 0.203519, + 0.019679, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.017278, + 0.033699, + 0.044479, + 0.207381, + 0.687609, + 0.522136, + 0.437237, + 0.07031, + 0.020314, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00941, + 0.144468, + 0.356397, + 0.490587, + 0.544575, + 0.620054, + 0.488555, + 0.380105, + 0.01217, + 0.002112, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.022786, + 0.047566, + 0.064313, + 0.071582, + 0.634551, + 0.556566, + 0.379696, + 0.004149, + 0.008831, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.021168, + 0.051712, + 0.068848, + 0.045496, + 0.66263, + 0.576375, + 0.413789, + 0.004349, + 0.007091, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.070206, + 0.163014, + 0.110901, + 0.495187, + 0.617703, + 0.507758, + 0.367585, + 0.046984, + 0.001311, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011559, + 0.031751, + 0.046834, + 0.054422, + 0.050921, + 0.038803, + 0.019734, + 0.001151, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.020599, + 0.046615, + 0.064086, + 0.071965, + 0.068121, + 0.055125, + 0.032241, + 0.001603, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011141, + 0.043965, + 0.046957, + 0.068122, + 0.065614, + 0.052609, + 0.02948, + 0.000373, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.089539, + 0.224285, + 0.059044, + 0.066816, + 0.482436, + 0.169357, + 0.027893, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014675, + 0.042424, + 0.143034, + 0.066805, + 0.42111, + 0.104975, + 0.189291, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014836, + 0.171546, + 0.279892, + 0.127122, + 0.425059, + 0.030478, + 0.057919, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.026126, + 0.238089, + 0.353839, + 0.284999, + 0.536789, + 0.451501, + 0.274572, + 0.047026, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.043562, + 0.163216, + 0.032265, + 0.171162, + 0.07991, + 0.024757, + 0.011067, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002917, + 0.029422, + 0.066345, + 0.124618, + 0.482904, + 0.426874, + 0.054648, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.036995, + 0.189801, + 0.300072, + 0.34967, + 0.469352, + 0.372083, + 0.201555, + 0.014336, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.023492, + 0.011569, + 0.303782, + 0.024098, + 0.022804, + 0.015757, + 0.028284, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.125727, + 0.29719, + 0.352283, + 0.493008, + 0.384698, + 0.209452, + 0.020207, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.029732, + 0.180347, + 0.295936, + 0.34337, + 0.502534, + 0.3986, + 0.00698, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.020451, + 0.036138, + 0.244611, + 0.243728, + 0.015156, + 0.015111, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001787, + 0.17029, + 0.281286, + 0.298473, + 0.51888, + 0.388314, + 0.189048, + 0.035775, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007536, + 0.015964, + 0.109947, + 0.183792, + 0.032551, + 0.005908, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001427, + 0.157248, + 0.212953, + 0.048592, + 0.033788, + 0.296564, + 0.096068, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.014817, + 0.028548, + 0.034313, + 0.032719, + 0.022794, + 0.002675, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010169, + 0.017192, + 0.021513, + 0.028049, + 0.012959, + 0.000295, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.0395, + 0.141847, + 0.032115, + 0.029385, + 0.018079, + 0.000704, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001372, + 0.107343, + 0.023299, + 0.103689, + 0.384717, + 0.053714, + 0.000282, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001119, + 0.108418, + 0.059457, + 0.232266, + 0.380823, + 0.281518, + 0.043834, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.059085, + 0.081585, + 0.054744, + 0.118027, + 0.011133, + 0.00024, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.019778, + 0.186088, + 0.1617, + 0.015643, + 0.013395, + 0.01939, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.073655, + 0.03126, + 0.188869, + 0.036871, + 0.09591, + 0.022425, + 0.005477, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003274, + 0.034363, + 0.074728, + 0.063289, + 0.015699, + 0.014634, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.049508, + 0.162703, + 0.21388, + 0.38478, + 0.183507, + 0.004587, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001828, + 0.018521, + 0.217768, + 0.407152, + 0.294484, + 0.086532, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.045821, + 0.163905, + 0.212804, + 0.428382, + 0.315751, + 0.005312, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.042808, + 0.020385, + 0.205535, + 0.410813, + 0.013044, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.0341, + 0.066203, + 0.016298, + 0.014617, + 0.076907, + 0.003869, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.03828, + 0.147722, + 0.101742, + 0.333808, + 0.014618, + 0.005986, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.011265, + 0.018513, + 0.016755, + 0.006164, + 0.009109, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.03241, + 0.13336, + 0.183468, + 0.385454, + 0.254885, + 0.094772, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000661, + 0.052564, + 0.084322, + 0.011865, + 0.019485, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.010214, + 0.018599, + 0.027909, + 0.025866, + 0.07154, + 0.04726, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.015988, + 0.025418, + 0.023396, + 0.007885, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.00106, + 0.052596, + 0.108047, + 0.222353, + 0.118266, + 0.021668, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.013217, + 0.102286, + 0.158754, + 0.356377, + 0.191032, + 0.076204, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006339, + 0.101272, + 0.162158, + 0.388648, + 0.206463, + 0.097421, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.020264, + 0.075706, + 0.374837, + 0.194224, + 0.087274, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000105, + 0.05534, + 0.026143, + 0.19636, + 0.168692, + 0.048457, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.007244, + 0.017688, + 0.016739, + 0.003091, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002381, + 0.008359, + 0.010021, + 0.001485, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003128, + 0.010868, + 0.010075, + 0.001126, + 0.008656, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004247, + 0.014463, + 0.013413, + 0.00236, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003539, + 0.013435, + 0.012415, + 0.002015, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000575, + 0.06554, + 0.13797, + 0.337923, + 0.000452, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004689, + 0.017843, + 0.01659, + 0.002936, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.001096, + 0.05585, + 0.098478, + 0.017851, + 0.003451, + 0.068568, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.053259, + 0.015084, + 0.152907, + 0.030905, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.004342, + 0.05783, + 0.132746, + 0.340582, + 0.18537, + 0.081265, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003173, + 0.055347, + 0.136139, + 0.203187, + 0.092414, + 0.073999, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.006143, + 0.11714, + 0.146112, + 0.091321, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002724, + 0.014735, + 0.013493, + 0.002342, + 0.03126, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.028129, + 0.055844, + 0.008398, + 0.001823, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003961, + 0.01751, + 0.017526, + 0.003196, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002867, + 0.011201, + 0.011159, + 0.002815, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000439, + 0.053742, + 0.131987, + 0.319499, + 0.167789, + 0.049425, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002103, + 0.056602, + 0.136422, + 0.322512, + 0.182682, + 0.074002, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002935, + 0.00103, + 0.015668, + 0.016955, + 0.024938, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.000479, + 0.00933, + 0.009388, + 0.003043, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.003, + 0.016376, + 0.016305, + 0.004154, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.002625, + 0.009451, + 0.010132, + 0.003635, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + "ElectricStorage": { + "max_kw": 1000, + "max_kwh": 10000, + "can_net_meter": true, + "can_wholesale": true, + "can_export_beyond_nem_limit": false + } +} \ No newline at end of file From 0a7c8319c69813f9fd356d5804ea4b150c3a0a43 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:02:53 -0600 Subject: [PATCH 32/66] update bess export testing --- test/development_tests.jl | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/test/development_tests.jl b/test/development_tests.jl index 72d988a78..23bccd98d 100644 --- a/test/development_tests.jl +++ b/test/development_tests.jl @@ -6,7 +6,7 @@ # Case 1: energy rate is lower during PV production # so expect battery to do energy arbitrage and NEM export during higher rate d = JSON.parsefile("./scenarios/bess_export.json") - d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.2, 0.1, 0.2], inner=8, outer=365) + d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.5, 0.1, 0.5], inner=8, outer=365) # d["ElectricStorage"]["can_net_meter"] = true # d["ElectricStorage"]["can_wholesale"] = true # d["PV"]["can_net_meter"] = false @@ -22,10 +22,11 @@ JSON.print(f, results, 4) end # @test results["PV"]["electric_to_grid_series_kw"] - println(sum(results["ElectricStorage"]["electric_to_grid_series_kw"])) - @test sum(results["ElectricStorage"]["electric_to_grid_series_kw"]) > 0 - @test m[:WHL_benefit] == 0 - @test m[:EXC_benefit] == 0 + @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 + @test all(x == 0.0 for (i,x) in enumerate(results["ElectricStorage"]["storage_to_grid_series_kw"]) if 8 < i % 24 < 17) + @test value(m[:NEM_benefit]) <= 0 + @test value(m[:WHL_benefit]) == 0 + @test value(m[:EXC_benefit]) == 0 @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 finalize(backend(m)) empty!(m) @@ -36,14 +37,17 @@ #TODO: have to limit PV size to get expected result? d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.1], outer=8760) d["ElectricTariff"]["wholesale_rate"] = 0.2 - d["ElectricTariff"]["allow_simultaneous_export_import"] = false + d["ElectricUtility"]["allow_simultaneous_export_import"] = false m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) + open("debug_results2.json","w") do f + JSON.print(f, results, 4) + end @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"]) - if results["ElectricStorage"]["electric_to_grid_series_kw"][i] > 0) - @test sum(results["ElectricStorage"]["electric_to_grid_series_kw"]) > 0 - @test m[:NEM_benefit] == 0 - @test m[:EXC_benefit] == 0 + if results["ElectricStorage"]["storage_to_grid_series_kw"][i] > 0) + @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 + @test value(m[:NEM_benefit]) == 0 + @test value(m[:EXC_benefit]) == 0 @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 finalize(backend(m)) empty!(m) From d522d0f0d71b122143874c4ce2cb650d224a55bd Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Tue, 17 Jun 2025 16:04:05 -0600 Subject: [PATCH 33/66] add comment to clarify bound --- src/constraints/electric_utility_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 29fca396c..55cec2d4e 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -132,7 +132,7 @@ function add_export_constraints(m, p; _n="") EXC_benefit = 0 if :EXC in p.s.electric_tariff.export_bins - EXC_benefit = @variable(m, lower_bound = max_bene) + EXC_benefit = @variable(m, lower_bound = max_bene) #lower bound because the benefit is treated as a negative cost if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) @constraint(m, binNEM => {EXC_benefit >= p.pwf_e * p.hours_per_time_step * From dbb5b178cd28663606933df02e5f687bcac90b75 Mon Sep 17 00:00:00 2001 From: Hallie Dunham <70401017+hdunham@users.noreply.github.com> Date: Wed, 18 Jun 2025 08:58:21 -0600 Subject: [PATCH 34/66] update bess export testing --- test/development_tests.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/development_tests.jl b/test/development_tests.jl index 23bccd98d..4a0563e99 100644 --- a/test/development_tests.jl +++ b/test/development_tests.jl @@ -35,8 +35,10 @@ # Case 2: whl rate > retail rate and allow_simultaneous_export_import = false # so expect battery to wholesale once load is met #TODO: have to limit PV size to get expected result? - d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.1], outer=8760) - d["ElectricTariff"]["wholesale_rate"] = 0.2 + d["ElectricStorage"]["can_net_meter"] = false + d["PV"]["can_net_meter"] = false + d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.1], 8760) + d["ElectricTariff"]["wholesale_rate"] = append!(repeat([5], 31*24), repeat([0], 8760 - 31*24)) d["ElectricUtility"]["allow_simultaneous_export_import"] = false m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) From cbecb6bba66fde7f515d01d26f988457b066276d Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 25 Jun 2025 12:09:35 -0600 Subject: [PATCH 35/66] ex update --- src/constraints/emissions_constraints.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constraints/emissions_constraints.jl b/src/constraints/emissions_constraints.jl index 5d8995aef..e40a7ba19 100644 --- a/src/constraints/emissions_constraints.jl +++ b/src/constraints/emissions_constraints.jl @@ -122,6 +122,8 @@ function calc_yr1_emissions_offset_from_elec_exports(m, p) yr1_emissions_offset_from_elec_exports_lbs_CO2 = @expression(m, p.hours_per_time_step * sum(m[:dvProductionToGrid][t,u,ts] * (p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh[ts]) for t in p.techs.elec, ts in p.time_steps, u in p.export_bins_by_tech[t]) + # + BESS export (similar to net metering calcs) + # sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in NEM_storage, ts in p.time_steps) ) # if battery ends up being able to discharge to grid, need to incorporate here- might require complex tracking of what's charging battery From 1b5e9eb4af3ad586f110168fc29df0dbfa429b16 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 25 Jun 2025 12:49:03 -0600 Subject: [PATCH 36/66] export in emissions --- src/constraints/emissions_constraints.jl | 23 ++++++++++--------- .../renewable_energy_constraints.jl | 11 +++++---- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/constraints/emissions_constraints.jl b/src/constraints/emissions_constraints.jl index e40a7ba19..20d1bd500 100644 --- a/src/constraints/emissions_constraints.jl +++ b/src/constraints/emissions_constraints.jl @@ -120,26 +120,27 @@ function calc_yr1_emissions_offset_from_elec_exports(m, p) return 0.0, 0.0, 0.0, 0.0 end yr1_emissions_offset_from_elec_exports_lbs_CO2 = @expression(m, p.hours_per_time_step * - sum(m[:dvProductionToGrid][t,u,ts] * (p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh[ts]) - for t in p.techs.elec, ts in p.time_steps, u in p.export_bins_by_tech[t]) - # + BESS export (similar to net metering calcs) - # sum( m[Symbol("dvStorageToGrid"*_n)][b, :NEM, ts] for b in NEM_storage, ts in p.time_steps) + (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) + * (p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh[ts]) for ts in p.time_steps ) - # if battery ends up being able to discharge to grid, need to incorporate here- might require complex tracking of what's charging battery yr1_emissions_offset_from_elec_exports_lbs_NOx = @expression(m, p.hours_per_time_step * - sum(m[:dvProductionToGrid][t,u,ts] * (p.s.electric_utility.emissions_factor_series_lb_NOx_per_kwh[ts]) - for t in p.techs.elec, ts in p.time_steps, u in p.export_bins_by_tech[t]) + (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) + * (p.s.electric_utility.emissions_factor_series_lb_NOx_per_kwh[ts]) for ts in p.time_steps ) yr1_emissions_offset_from_elec_exports_lbs_SO2 = @expression(m, p.hours_per_time_step * - sum(m[:dvProductionToGrid][t,u,ts] * (p.s.electric_utility.emissions_factor_series_lb_SO2_per_kwh[ts]) - for t in p.techs.elec, ts in p.time_steps, u in p.export_bins_by_tech[t]) + (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) + * (p.s.electric_utility.emissions_factor_series_lb_SO2_per_kwh[ts]) for ts in p.time_steps ) yr1_emissions_offset_from_elec_exports_lbs_PM25 = @expression(m, p.hours_per_time_step * - sum(m[:dvProductionToGrid][t,u,ts] * (p.s.electric_utility.emissions_factor_series_lb_PM25_per_kwh[ts]) - for t in p.techs.elec, ts in p.time_steps, u in p.export_bins_by_tech[t]) + (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) + * (p.s.electric_utility.emissions_factor_series_lb_PM25_per_kwh[ts]) for ts in p.time_steps ) return yr1_emissions_offset_from_elec_exports_lbs_CO2, diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 294f918e9..514cb9d26 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -58,8 +58,6 @@ function add_re_elec_calcs(m,p) @warn("The presence of a steam turbine may cause the renewable energy fraction to be inaccurate.") end - # Note: when we add capability for battery to discharge to grid, need to make sure only RE that is being consumed - # onsite is counted so battery doesn't become a back door for RE to grid. m[:AnnualOnsiteREEleckWh] = @expression(m, p.hours_per_time_step * ( #total RE elec generation, excl steam turbine sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[:dvRatedProduction][t,ts] * @@ -78,13 +76,14 @@ function add_re_elec_calcs(m,p) - (1 - p.s.site.include_exported_renewable_electricity_in_total) * sum(m[:dvProductionToGrid][t,u,ts]*p.tech_renewable_energy_fraction[t] for t in setdiff(p.techs.elec, p.techs.steam_turbine), u in p.export_bins_by_tech[t], ts in p.time_steps - ) + ) + # TODO: battery can now discharge to the grid, but this export is not yet accounted for in the RE calculation. + # This is only relevant if include_exported_renewable_electricity_in_total = false. If false, the + # battery could become a "back door" for export of RE to the grid since it's not subtracted here. Will require tracking of RE in BESS. ) # + SteamTurbineAnnualREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step ) - # Note: when we add capability for battery to discharge to grid, need to subtract out *grid RE* discharged from battery - # back to grid so that loop doesn't become a back door for increasing RE. This will require some careful thought! m[:AnnualGridREEleckWh] = @expression(m, p.hours_per_time_step * ( # renewable energy from grid sum(m[:dvGridPurchase][ts, tier] * p.s.electric_utility.renewable_energy_fraction_series[ts] @@ -94,6 +93,8 @@ function add_re_elec_calcs(m,p) (1 - p.s.storage.attr[b].charge_efficiency * p.s.storage.attr[b].discharge_efficiency) for ts in p.time_steps, b in p.s.storage.types.elec ) + # TODO: battery can now discharge to the grid, but this export is not yet accounted for in the RE calculation. + # Need to subtract out *grid RE* discharged from battery back to grid so that loop doesn't become a back door for increasing RE. This will require some careful thought! ) ) From 63a214b47de4202a94f2e9024d30f69666b3da35 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 3 Jul 2025 10:14:59 -0600 Subject: [PATCH 37/66] Update emissions_constraints.jl --- src/constraints/emissions_constraints.jl | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/constraints/emissions_constraints.jl b/src/constraints/emissions_constraints.jl index 20d1bd500..d5dea1131 100644 --- a/src/constraints/emissions_constraints.jl +++ b/src/constraints/emissions_constraints.jl @@ -120,27 +120,35 @@ function calc_yr1_emissions_offset_from_elec_exports(m, p) return 0.0, 0.0, 0.0, 0.0 end yr1_emissions_offset_from_elec_exports_lbs_CO2 = @expression(m, p.hours_per_time_step * - (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) - + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) - * (p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh[ts]) for ts in p.time_steps + sum( p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh[ts] * ( + sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + ) for ts in p.time_steps + ) ) yr1_emissions_offset_from_elec_exports_lbs_NOx = @expression(m, p.hours_per_time_step * - (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) - + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) - * (p.s.electric_utility.emissions_factor_series_lb_NOx_per_kwh[ts]) for ts in p.time_steps + sum( p.s.electric_utility.emissions_factor_series_lb_NOx_per_kwh[ts] * ( + sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + ) for ts in p.time_steps + ) ) yr1_emissions_offset_from_elec_exports_lbs_SO2 = @expression(m, p.hours_per_time_step * - (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) - + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) - * (p.s.electric_utility.emissions_factor_series_lb_SO2_per_kwh[ts]) for ts in p.time_steps + sum( p.s.electric_utility.emissions_factor_series_lb_SO2_per_kwh[ts] * ( + sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + ) for ts in p.time_steps + ) ) yr1_emissions_offset_from_elec_exports_lbs_PM25 = @expression(m, p.hours_per_time_step * - (sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) - + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b])) - * (p.s.electric_utility.emissions_factor_series_lb_PM25_per_kwh[ts]) for ts in p.time_steps + sum( p.s.electric_utility.emissions_factor_series_lb_PM25_per_kwh[ts] * ( + sum(m[:dvProductionToGrid][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[:dvStorageToGrid][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + ) for ts in p.time_steps + ) ) return yr1_emissions_offset_from_elec_exports_lbs_CO2, From 002673875c34053ffc5b05878b34ea219c50722c Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 3 Jul 2025 10:44:52 -0600 Subject: [PATCH 38/66] bigM_hourly_load_plus_battery --- src/constraints/electric_utility_constraints.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 55cec2d4e..62c833725 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -33,7 +33,7 @@ function add_export_constraints(m, p; _n="") if p.s.electric_utility.net_metering_limit_kw == p.s.electric_utility.interconnection_limit_kw && isempty(vcat(WHL_techs, WHL_storage)) # no need for binNEM nor binWHL binNEM = 1 - # TODO: Decide whether to ignore BESS size in this constraint? + # Note: BESS can export but is not included in interconnection limit. @constraint(m, sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.interconnection_limit_kw ) @@ -65,8 +65,8 @@ function add_export_constraints(m, p; _n="") NEM_benefit = @variable(m, lower_bound = max_bene) # If choosing to take advantage of NEM, must have total capacity less than net_metering_limit_kw + # Note: BESS can export but is not included in net_metering_limit_kw or interconnection_limit_kw. if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) - # TODO: Decide whether to ignore BESS size in these constraints? @constraint(m, binNEM => {sum(m[Symbol("dvSize"*_n)][t] for t in NEM_techs) <= p.s.electric_utility.net_metering_limit_kw} ) @@ -319,16 +319,15 @@ function add_simultaneous_export_import_constraint(m, p; _n="") } ) else - #TODO: consider adding max battery power here like in https://github.com/NREL/REopt.jl/pull/447/files - bigM_hourly_load = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal) + bigM_hourly_load_plus_battery = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal)+sum(Real[p.s.storage.attr[b].max_kw for b in p.s.storage.types.elec]) @constraint(m, NoGridPurchasesBinary[ts in p.time_steps], sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) + - sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) <= bigM_hourly_load*(1-m[Symbol("binNoGridPurchases"*_n)][ts]) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) <= bigM_hourly_load_plus_battery*(1-m[Symbol("binNoGridPurchases"*_n)][ts]) ) @constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps], sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) - <= bigM_hourly_load * m[Symbol("binNoGridPurchases"*_n)][ts] + <= bigM_hourly_load_plus_battery * m[Symbol("binNoGridPurchases"*_n)][ts] ) end end From 875cbd25cad3acf194222234fd02a7462923fa0c Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 3 Jul 2025 11:18:42 -0600 Subject: [PATCH 39/66] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 06b08875d..5dc60e193 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -291,12 +291,15 @@ struct ElectricStorage <: AbstractElectricStorage @warn "Battery replacement costs (per_kwh) will not be considered because battery_replacement_year is greater than or equal to analysis_years." end - # if s.off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) - # @warn "Setting ElectricStorage can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." - # can_net_meter = false - # can_wholesale = false - # can_export_beyond_nem_limit = false - # end + can_net_meter = s.can_net_meter + can_wholesale = s.can_wholesale + can_export_beyond_nem_limit = s.can_export_beyond_nem_limit + if s.off_grid_flag && (can_net_meter || can_wholesale || can_export_beyond_nem_limit) + @warn "Setting ElectricStorage can_net_meter, can_wholesale, and can_export_beyond_nem_limit to False because `off_grid_flag` is true." + can_net_meter = false + can_wholesale = false + can_export_beyond_nem_limit = false + end # copy the replace_costs in case we need to change them replace_cost_per_kw = s.replace_cost_per_kw @@ -357,9 +360,9 @@ struct ElectricStorage <: AbstractElectricStorage s.soc_min_applies_during_outages, s.soc_init_fraction, s.can_grid_charge, - s.can_net_meter, - s.can_wholesale, - s.can_export_beyond_nem_limit, + can_net_meter, + can_wholesale, + can_export_beyond_nem_limit, s.installed_cost_per_kw, s.installed_cost_per_kwh, replace_cost_per_kw, From e689e0fd0e57281f48678a0105b9ffd238a64761 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 3 Jul 2025 11:54:02 -0600 Subject: [PATCH 40/66] try multi_node --- src/core/reopt_inputs.jl | 1 - src/core/reopt_multinode.jl | 6 ++++-- src/mpc/model_multinode.jl | 7 ++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 4f6631d49..a2036c91b 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -379,7 +379,6 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) pbi_benefit_per_kwh = Dict{String, Any}() # export related inputs - @info "s.electric_tariff.export_bins: $(s.electric_tariff.export_bins)" techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) export_bins_by_tech = Dict{String, Array{Symbol, 1}}() storage_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) diff --git a/src/core/reopt_multinode.jl b/src/core/reopt_multinode.jl index 7e12f843e..ef35b7fb8 100644 --- a/src/core/reopt_multinode.jl +++ b/src/core/reopt_multinode.jl @@ -16,8 +16,7 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T} "dvStorageEnergy", ] dvs_idx_on_storagetypes_time_steps = String[ - "dvDischargeFromStorage", - "dvStorageToGrid" + "dvDischargeFromStorage" ] for p in ps _n = string("_", p.s.site.node) @@ -65,6 +64,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T} if !isempty(p.s.electric_tariff.export_bins) dv = "dvProductionToGrid"*_n m[Symbol(dv)] = @variable(m, [p.techs.elec, p.s.electric_tariff.export_bins, p.time_steps], base_name=dv, lower_bound=0) + dv = "dvStorageToGrid"*_n + m[Symbol(dv)] = @variable(m, [p.s.storage.types.elec, p.s.electric_tariff.export_bins, p.time_steps], base_name=dv, lower_bound=0) end ex_name = "TotalTechCapCosts"*_n @@ -127,6 +128,7 @@ function build_reopt!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}}) m[Symbol("dvProductionToStorage"*_n)][b, t, ts] == 0) @constraint(m, [ts in p.time_steps], m[Symbol("dvDischargeFromStorage"*_n)][b, ts] == 0) @constraint(m, [ts in p.time_steps], m[Symbol("dvGridToStorage"*_n)][b, ts] == 0) + @constraint(m, [u in p.export_bins_by_storage[b], ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, u, ts] == 0) else add_storage_size_constraints(m, p, b; _n=_n) add_general_storage_dispatch_constraints(m, p, b; _n=_n) diff --git a/src/mpc/model_multinode.jl b/src/mpc/model_multinode.jl index 3748dc538..e36c38962 100644 --- a/src/mpc/model_multinode.jl +++ b/src/mpc/model_multinode.jl @@ -66,7 +66,7 @@ function build_mpc!(m::JuMP.AbstractModel, ps::AbstractVector{MPCInputs}) @constraint(m, [ts in p.time_steps], m[Symbol("dvDischargeFromStorage"*_n)][b, ts] == 0) if b in p.s.storage.types.elec @constraint(m, [ts in p.time_steps], m[Symbol("dvGridToStorage"*_n)][b, ts] == 0) - # TODO: add constraint on storage to grid here + @constraint(m, [u in p.export_bins_by_storage[b], ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, u, ts] == 0) end else add_general_storage_dispatch_constraints(m, p, b; _n=_n) @@ -153,8 +153,7 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{MPCInputs}) "dvRatedProduction", ] dvs_idx_on_storagetypes_time_steps = String[ - "dvDischargeFromStorage", - "dvStorageToGrid" + "dvDischargeFromStorage" ] for p in ps _n = string("_", p.s.node) @@ -202,6 +201,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{MPCInputs}) if !isempty(p.s.electric_tariff.export_bins) dv = "dvProductionToGrid"*_n m[Symbol(dv)] = @variable(m, [p.techs.elec, p.s.electric_tariff.export_bins, p.time_steps], base_name=dv, lower_bound=0) + dv = "dvStorageToGrid"*_n + m[Symbol(dv)] = @variable(m, [p.s.storage.types.elec, p.s.electric_tariff.export_bins, p.time_steps], base_name=dv, lower_bound=0) end ex_name = "TotalPerUnitProdOMCosts"*_n From 63364664061993eca8da03e3b2ee261c99f350fe Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 3 Jul 2025 12:26:01 -0600 Subject: [PATCH 41/66] t to b --- src/core/reopt.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index eca082d04..9b9a0ee9f 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -194,8 +194,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) fix(m[:dvGridPurchase][ts, tier] , 0.0, force=true) end - for t in p.s.storage.types.elec - fix(m[:dvGridToStorage][t, ts], 0.0, force=true) + for b in p.s.storage.types.elec + fix(m[:dvGridToStorage][b, ts], 0.0, force=true) end if !isempty(p.s.electric_tariff.export_bins) From ee1a4f1146843a0f01187217b1c7c84bd85ec97c Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 7 Jul 2025 11:14:45 -0600 Subject: [PATCH 42/66] Update electric_storage.jl --- src/results/electric_storage.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index 9238856bd..39b2dafac 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -31,17 +31,16 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: soc = (m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) r["soc_series_fraction"] = round.(value.(soc) ./ r["size_kwh"], digits=3) + r["storage_to_grid_series_kw"] = zeros(size(r["soc_series_fraction"])) if !isempty(p.s.electric_tariff.export_bins) StorageToGrid = @expression(m, [ts in p.time_steps], sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for u in p.export_bins_by_storage[b])) - else - StorageToGrid = repeat([0], length(p.time_steps)) + r["storage_to_grid_series_kw"] = round.(value.(StorageToGrid), digits=3).data end StorageToLoad = ( m[Symbol("dvDischargeFromStorage"*_n)][b, ts] - - StorageToGrid[ts] for ts in p.time_steps - ) - r["storage_to_grid_series_kw"] = round.(value.(StorageToGrid), digits=3) + - r["storage_to_grid_series_kw"][ts] for ts in p.time_steps + ) r["storage_to_load_series_kw"] = round.(value.(StorageToLoad), digits=3) r["initial_capital_cost"] = r["size_kwh"] * p.s.storage.attr[b].installed_cost_per_kwh + From e1a504d6473523eabf27c51262dd854e8b7abfaa Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 9 Jul 2025 12:41:15 -0600 Subject: [PATCH 43/66] updt soc_init and add tol input --- src/constraints/storage_constraints.jl | 9 +++++---- src/core/energy_storage/electric_storage.jl | 18 +++++++++++++----- src/core/reopt.jl | 2 +- src/mpc/structs.jl | 2 ++ test/runtests.jl | 1 + 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index e9f44e300..a394c431d 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -43,7 +43,7 @@ function add_general_storage_dispatch_constraints(m, p, b; _n="") @constraint(m, m[Symbol("dvStoredEnergy"*_n)][b, 0] == m[:dvStoredEnergy][b, maximum(p.time_steps)] ) - elseif !hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) || isnothing(p.s.storage.attr[b].fixed_soc_series_fraction) + else @constraint(m, m[Symbol("dvStoredEnergy"*_n)][b, 0] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b] ) @@ -138,12 +138,13 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") # Constrain to fixed_soc_series_fraction if hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) && !isnothing(p.s.storage.attr[b].fixed_soc_series_fraction) + # Allow for a percentage point (fractional) buffer on user-provided fixed_soc_series_fraction + buffer = 0.09 @constraint(m, [ts in p.time_steps], - # Allow for a 1 pct point buffer on user-provided fixed_soc_series_fraction - m[Symbol("dvStoredEnergy"*_n)][b, ts] <= (0.02 + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b] + m[Symbol("dvStoredEnergy"*_n)][b, ts] <= (p.s.storage.attr[b].fixed_soc_series_fraction_tolerance + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b] ) @constraint(m, [ts in p.time_steps], - m[Symbol("dvStoredEnergy"*_n)][b, ts] >= (-0.02 + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b] + m[Symbol("dvStoredEnergy"*_n)][b, ts] >= (-p.s.storage.attr[b].fixed_soc_series_fraction_tolerance + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b] ) end end diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index f87139da5..c1f511e31 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -202,7 +202,8 @@ end degradation::Dict = Dict() minimum_avg_soc_fraction::Float64 = 0.0 optimize_soc_init_fraction::Bool = false # If true, soc_init_fraction will not apply. Model will optimize initial SOC and constrain initial SOC = final SOC. - fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing # If provided, SOC (as fraction of total energy capacity) will not be optimized and will instead be fixed to the values provided here +- 0.02 (this buffer is to avoid infeasible solutions) + fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing # If provided, SOC (as fraction of total energy capacity) will not be optimized and will instead be fixed to the values provided here +- 0.02 (this buffer is to avoid infeasible solutions) + fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.1 : 0.0 # +- absolute tolerance on fixed_soc_series_fraction to avoid infeasible solutions. min_duration_hours::Real = 0.0 # Minimum amount of time storage can discharge at its rated power capacity max_duration_hours::Real = 100000.0 # Maximum amount of time storage can discharge at its rated power capacity (ratio of ElectricStorage size_kwh to size_kw) @@ -250,6 +251,7 @@ Base.@kwdef struct ElectricStorageDefaults min_duration_hours::Real = 0.0 max_duration_hours::Real = 100000.0 fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing + fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.1 : 0.0 end @@ -302,7 +304,8 @@ struct ElectricStorage <: AbstractElectricStorage optimize_soc_init_fraction::Bool min_duration_hours::Real max_duration_hours::Real - fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} + fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} + fixed_soc_series_fraction_tolerance::Real function ElectricStorage(d::Dict, f::Financial) @@ -331,12 +334,16 @@ struct ElectricStorage <: AbstractElectricStorage end # Copy SOC input in case we need to change them + soc_init_fraction = s.soc_init_fraction soc_min_fraction = s.soc_min_fraction optimize_soc_init_fraction = s.optimize_soc_init_fraction + minimum_avg_soc_fraction = s.minimum_avg_soc_fraction if !isnothing(s.fixed_soc_series_fraction) @warn "Fixing ElectricStorage soc_series_fraction to the provided fixed_soc_series_fraction. Other SOC inputs will be ignored." + soc_init_fraction = s.fixed_soc_series_fraction[1] soc_min_fraction = 0.0 optimize_soc_init_fraction = false + minimum_avg_soc_fraction = 0.0 error_if_series_vals_not_0_to_1(s.fixed_soc_series_fraction, "ElectricStorage", "fixed_soc_series_fraction") end @@ -428,7 +435,7 @@ struct ElectricStorage <: AbstractElectricStorage s.rectifier_efficiency_fraction, soc_min_fraction, s.soc_min_applies_during_outages, - s.soc_init_fraction, + soc_init_fraction, s.can_grid_charge, can_net_meter, can_wholesale, @@ -457,11 +464,12 @@ struct ElectricStorage <: AbstractElectricStorage net_present_cost_cost_constant, s.model_degradation, degr, - s.minimum_avg_soc_fraction, + minimum_avg_soc_fraction, optimize_soc_init_fraction, s.min_duration_hours, s.max_duration_hours, - s.fixed_soc_series_fraction + s.fixed_soc_series_fraction, + s.fixed_soc_series_fraction_tolerance ) end end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 9b9a0ee9f..5c92c89bf 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -444,7 +444,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) degr_bool = p.s.storage.attr[b].model_degradation if degr_bool - @info "Battery energy capacity degradation costs for $b are being modeled using REopt's Degradation model. ElectricStorageOMCost will include costs to be incurred for power electronics and the cost constant." + @info "Battery energy capacity degradation costs for $b are being modeled using REopt's Degradation model. ElectricStorageOMCost will include costs incurred for power electronics [per kW] and the cost constant, but not for the per kWh components." add_to_expression!( m[:ElectricStorageOMCost], -1.0 * p.third_party_factor * p.pwf_om * p.s.storage.attr[b].om_cost_fraction_of_installed_cost * p.s.storage.attr[b].installed_cost_per_kwh * m[:dvStorageEnergy][b] ) diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index 746fc9105..a52e4ae6b 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -234,6 +234,7 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage max_kwh::Float64 = size_kwh minimum_avg_soc_fraction::Float64 = 0.0 fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing + fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.1 : 0.0 end ``` """ @@ -253,6 +254,7 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage max_kwh::Float64 = size_kwh minimum_avg_soc_fraction::Float64 = 0.0 fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing + fixed_soc_series_fraction_tolerance::Real = !isnothing(fixed_soc_series_fraction) ? 0.1 : 0.0 end diff --git a/test/runtests.jl b/test/runtests.jl index 7b1226c66..c742c7275 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3810,6 +3810,7 @@ else # run HiGHS tests # Fix soc_series to optimal from previous run m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) post["ElectricStorage"]["fixed_soc_series_fraction"] = soc_series + post["ElectricStorage"]["fixed_soc_series_fraction_tolerance"] = 0.02 results = run_reopt(m1 , post) lcc2 = results["Financial"]["lcc"] From dfe1ed0266d54001a1b2745a54b6b818c0dee4d7 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 17 Jul 2025 11:03:55 -0600 Subject: [PATCH 44/66] output name --- src/constraints/storage_constraints.jl | 1 - src/results/electric_storage.jl | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index a394c431d..e6b967f5b 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -139,7 +139,6 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="") # Constrain to fixed_soc_series_fraction if hasproperty(p.s.storage.attr[b], :fixed_soc_series_fraction) && !isnothing(p.s.storage.attr[b].fixed_soc_series_fraction) # Allow for a percentage point (fractional) buffer on user-provided fixed_soc_series_fraction - buffer = 0.09 @constraint(m, [ts in p.time_steps], m[Symbol("dvStoredEnergy"*_n)][b, ts] <= (p.s.storage.attr[b].fixed_soc_series_fraction_tolerance + p.s.storage.attr[b].fixed_soc_series_fraction[ts]) * m[Symbol("dvStorageEnergy"*_n)][b] ) diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index 39b2dafac..ddf160698 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -8,7 +8,7 @@ - `storage_to_grid_series_kw` Vector of power exported to the grid over an average year - `initial_capital_cost` Upfront capital cost for storage and inverter # The following results are reported if storage degradation is modeled: -- `state_of_health` +- `state_of_health_series_fraction` - `maintenance_cost` - `replacement_month` # only applies is maintenance_strategy = "replacement" - `residual_value` @@ -48,7 +48,7 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: p.s.storage.attr[b].installed_cost_constant if p.s.storage.attr[b].model_degradation - r["state_of_health"] = round.(value.(m[:SOH]).data / value.(m[:dvStorageEnergy])["ElectricStorage"], digits=3) + r["state_of_health_series_fraction"] = round.(value.(m[:SOH]).data / value.(m[:dvStorageEnergy])["ElectricStorage"], digits=3) r["maintenance_cost"] = value(m[:degr_cost]) if p.s.storage.attr[b].degradation.maintenance_strategy == "replacement" r["replacement_month"] = round(Int, value( @@ -58,9 +58,9 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: # Determine fraction of useful life left assuming same replacement frequency. # Multiply by 0.2 to scale residual useful life since entire BESS is replaced when SOH drops below 80%. # Total BESS capacity residual is (0.8 + residual useful fraction) * BESS capacity - # If not replacements happen then useful capacity is SOH[end]*BESS capacity. + # If no replacements happen then useful capacity is SOH[end]*BESS capacity. if iszero(r["replacement_month"]) - r["total_residual_kwh"] = r["state_of_health"][end]*r["size_kwh"] + r["total_residual_kwh"] = r["state_of_health_series_fraction"][end]*r["size_kwh"] else # SOH[end] can be negative, so alternate method to calculate residual healthy SOH. total_replacements = (p.s.financial.analysis_years*12)/r["replacement_month"] From 809f0797639e49027e1a7ad60950a447ca89bd48 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 6 Aug 2025 12:07:51 -0600 Subject: [PATCH 45/66] initial add of grid cost --- src/constraints/load_balance.jl | 10 +++++++++- .../renewable_energy_constraints.jl | 2 ++ src/core/electric_utility.jl | 18 +++++++++++++++++- src/core/reopt.jl | 13 ++++++++++++- src/core/utils.jl | 1 + src/results/electric_utility.jl | 2 +- 6 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 7e507fba0..aad4414e4 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -97,7 +97,6 @@ function add_production_constraints(m, p; _n="") <= p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t, ts] ) - end @@ -155,4 +154,13 @@ function add_thermal_load_constraints(m, p; _n="") ) end end +end + +function calculate_net_load(m, p; _n="") + # Calculate net load (grid purchase - production to grid - BESS to grid) + @constraint(m, [ts in p.time_steps], + m[Symbol("dvNetLoad"*_n)][ts] == sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) + - sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for t in setdiff(p.techs.elec, p.techs.steam_turbine), u in p.export_bins_by_tech[t]) + - sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + ) end \ No newline at end of file diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index 514cb9d26..833a430b8 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -95,6 +95,8 @@ function add_re_elec_calcs(m,p) ) # TODO: battery can now discharge to the grid, but this export is not yet accounted for in the RE calculation. # Need to subtract out *grid RE* discharged from battery back to grid so that loop doesn't become a back door for increasing RE. This will require some careful thought! + + # TODO: Should this subtract out exported energy * renewable_energy_fraction_series? ) ) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index e180683d6..83a335bc0 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -54,6 +54,9 @@ ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) + + # Timeseries grid cost input + utility_grid_cost_per_kw_series::Array{<:Real,1} = Float64[] # Timeseries of per kW grid costs (incurred by utility, not the site). ``` !!! note "Outage modeling" @@ -113,6 +116,10 @@ - By default, REopt uses *clean energy fraction* for the region in which the site is located. - For sites outside of CONUS: REopt does not have default grid clean energy fraction data. Users must supply a custom `renewable_energy_fraction_series` + **Utility Grid Costs** + - utility_grid_cost_per_kw_series timeseries will be multiplied by the net load in each time step, and the analysis period total grid cost will be calculated using a pwf, assuming costs escalate at the elec_cost_escalation_rate_fraction. + - If provided, total grid cost will be included in the objective function (and impact dispatch) + """ struct ElectricUtility avert_emissions_region::String # AVERT emissions region @@ -139,6 +146,7 @@ struct ElectricUtility scenarios::Union{Nothing, UnitRange} net_metering_limit_kw::Real interconnection_limit_kw::Real + utility_grid_cost_per_kw_series::Array{<:Real,1} function ElectricUtility(; @@ -196,6 +204,9 @@ struct ElectricUtility ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) + + # Timeseries grid cost input + utility_grid_cost_per_kw_series::Array{<:Real,1} = Float64[] # Timeseries of per kW grid costs (incurred by utility, not the site). ) is_MPC = isnothing(latitude) || isnothing(longitude) @@ -343,6 +354,10 @@ struct ElectricUtility throw(@error("Sum of ElectricUtility inputs outage_probabilities must be equal to 1")) end + if !isempty(utility_grid_cost_per_kw_series) && length(utility_grid_cost_per_kw_series) != 8760*time_steps_per_hour + throw(@error("Length of utility_grid_cost_per_kw_series must be $(8760*time_steps_per_hour).")) + end + new( is_MPC ? "" : avert_emissions_region, is_MPC || isnothing(meters_to_region) ? typemax(Int64) : meters_to_region, @@ -365,7 +380,8 @@ struct ElectricUtility outage_time_steps, scenarios, net_metering_limit_kw, - interconnection_limit_kw + interconnection_limit_kw, + utility_grid_cost_per_kw_series ) end end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 5c92c89bf..baffa9294 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -279,6 +279,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:ExistingChillerCost] = 0.0 m[:ElectricStorageCapCost] = 0.0 m[:ElectricStorageOMCost] = 0.0 + m[:TotalUtilityGridCost] = 0.0 if !isempty(p.techs.all) || !isempty(p.techs.ghp) if !isempty(p.techs.all) @@ -522,6 +523,12 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) initial_capex_no_incentives(m, p) if !isnothing(p.s.financial.min_initial_capital_costs_before_incentives) || !isnothing(p.s.financial.max_initial_capital_costs_before_incentives) add_capex_constraints(m, p) + end + + # Calculate total grid cost based on utility grid cost per kW series and net load + if !isempty(p.s.electric_utility.utility_grid_cost_per_kw_series) + calculate_net_load(m, p) + m[:TotalUtilityGridCost] = p.pwf_e * sum(p.s.electric_utility.utility_grid_cost_per_kw_series .* m[:dvNetLoad]) end ################################# Objective Function ######################################## @@ -557,7 +564,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:AvoidedCapexByGHP] - m[:ResidualGHXCapCost] - # Subtract capital expenditures avoided by inclusion of ASHP - m[:AvoidedCapexByASHP] + m[:AvoidedCapexByASHP] + + + # Add utility grid cost (not incurred by customer) - only nonzero if utility_grid_cost_per_kw_series provided + m[:TotalUtilityGridCost] ); if !isempty(p.s.electric_utility.outage_durations) @@ -678,6 +688,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) dvPeakDemandMonth[p.months, 1:p.s.electric_tariff.n_monthly_demand_tiers] >= 0 # Peak electrical power demand during month m [kW] MinChargeAdder >= 0 binGHP[p.ghp_options], Bin # Can be <= 1 if require_ghp_purchase=0, and is ==1 if require_ghp_purchase=1 + dvNetLoad[p.time_steps] # Net load after on-site generation and storage [kW] end if !isempty(p.s.storage.types.elec) diff --git a/src/core/utils.jl b/src/core/utils.jl index 3d2b8ca6a..2341a43e9 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -165,6 +165,7 @@ function dictkeys_tosymbols(d::Dict) "emissions_factor_series_lb_SO2_per_kwh", "emissions_factor_series_lb_PM25_per_kwh", "renewable_energy_fraction_series", + "utility_grid_cost_per_kw_series", "heating_cop_reference", "heating_cf_reference", "heating_reference_temps_degF", diff --git a/src/results/electric_utility.jl b/src/results/electric_utility.jl index 79ab20507..62e21421e 100644 --- a/src/results/electric_utility.jl +++ b/src/results/electric_utility.jl @@ -46,7 +46,7 @@ function add_electric_utility_results(m::JuMP.AbstractModel, p::AbstractInputs, for ts in p.time_steps, tier in 1:p.s.electric_tariff.n_energy_tiers) r["annual_energy_supplied_kwh"] = round(value(Year1UtilityEnergy), digits=2) - if !isempty(p.s.storage.types.elec) + if !isempty(p.s.storage.types.elec) GridToLoad = (sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) for ts in p.time_steps) From e8793c780d7c838037aaf6ff3a5b000cf29f2cec Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 6 Aug 2025 12:56:37 -0600 Subject: [PATCH 46/66] updt documentation --- CHANGELOG.md | 4 +--- src/core/electric_tariff.jl | 2 +- src/core/electric_utility.jl | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a5cfa8c9..00ebae7c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,8 @@ Classify the change according to the following categories: ## bess-export ### Added - Add decision variable **dvStorageToGrid** and **ElectricStorage** input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` - -## fixed-bess-soc -### Added - Add **ElectricStorage** input field `fixed_soc_series_fraction` to allow users to fix the SOC timeseries +- Add **ElectricUtility** input field `utility_grid_cost_per_kw_series`. When provided, the TotalUtilityGridCost (lifecycle cost) is included in the LCC and objective function, but IS NOT included in the assumed site energy costs. ## test-runners ## Develop diff --git a/src/core/electric_tariff.jl b/src/core/electric_tariff.jl index 221b97802..f19c3dd2d 100644 --- a/src/core/electric_tariff.jl +++ b/src/core/electric_tariff.jl @@ -45,7 +45,7 @@ end urdb_response::Dict=Dict(), # Response JSON for URDB rates. Note: if creating your own urdb_response, ensure periods are zero-indexed. urdb_utility_name::String="", urdb_rate_name::String="", - 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. + wholesale_rate::T1=nothing, # Price of electricity [\$ per kWh] 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 diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 83a335bc0..1bdcc9dd0 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -56,7 +56,7 @@ renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) # Timeseries grid cost input - utility_grid_cost_per_kw_series::Array{<:Real,1} = Float64[] # Timeseries of per kW grid costs (incurred by utility, not the site). + utility_grid_cost_per_kw_series::Array{<:Real,1} = Float64[] # Timeseries of per kW grid costs (incurred by utility, not the site). # TODO: this should be \$ per kWh like wholesale_rate etc; will require multiplying by hours per timestep somewhere ``` !!! note "Outage modeling" From 027fb3a4d1db496f8f231047541f99b50b4fc0b6 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 21 Aug 2025 15:35:14 -0600 Subject: [PATCH 47/66] Update utils.jl --- src/core/utils.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/utils.jl b/src/core/utils.jl index 2341a43e9..e8a43f68e 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -172,6 +172,8 @@ function dictkeys_tosymbols(d::Dict) "cooling_cop_reference", "cooling_cf_reference", "cooling_reference_temps_degF", + "cycle_fade_coefficient", + "cycle_fade_fraction", #for ERP "pv_production_factor_series", "wind_production_factor_series", "battery_starting_soc_series_fraction", From a974c66baf9414dd652c5af5c04c95f6f09940e9 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 5 May 2026 11:14:54 -0600 Subject: [PATCH 48/66] rm grid cost this is all now in cost-to-utility branch --- CHANGELOG.md | 2 -- src/core/electric_utility.jl | 7 ------- src/core/reopt.jl | 12 +----------- src/core/utils.jl | 1 - 4 files changed, 1 insertion(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e175c69b1..265251210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,6 @@ Classify the change according to the following categories: ## bess-export ### Added - Add decision variable **dvStorageToGrid** and **ElectricStorage** input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` -- Add **ElectricStorage** input field `fixed_soc_series_fraction` to allow users to fix the SOC timeseries -- Add **ElectricUtility** input field `utility_grid_cost_per_kw_series`. When provided, the TotalUtilityGridCost (lifecycle cost) is included in the LCC and objective function, but IS NOT included in the assumed site energy costs. ## v0.59.0 ### Added diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 55ad4c600..20602f160 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -54,9 +54,6 @@ ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) - - # Timeseries grid cost input - utility_grid_cost_per_kw_series::Array{<:Real,1} = Float64[] # Timeseries of per kW grid costs (incurred by utility, not the site). # TODO: this should be \$ per kWh like wholesale_rate etc; will require multiplying by hours per timestep somewhere ``` !!! note "Outage modeling" @@ -116,10 +113,6 @@ - By default, REopt uses *clean energy fraction* for the region in which the site is located. - For sites outside of CONUS: REopt does not have default grid clean energy fraction data. Users must supply a custom `renewable_energy_fraction_series` - **Utility Grid Costs** - - utility_grid_cost_per_kw_series timeseries will be multiplied by the net load in each time step, and the analysis period total grid cost will be calculated using a pwf, assuming costs escalate at the elec_cost_escalation_rate_fraction. - - If provided, total grid cost will be included in the objective function (and impact dispatch) - """ struct ElectricUtility avert_emissions_region::String # AVERT emissions region diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 6554ed384..31fedcf64 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -263,7 +263,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:ExistingChillerCost] = 0.0 m[:ElectricStorageCapCost] = 0.0 m[:ElectricStorageOMCost] = 0.0 - m[:TotalUtilityGridCost] = 0.0 if !isempty(p.techs.all) || !isempty(p.techs.ghp) if !isempty(p.techs.all) @@ -506,12 +505,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_capex_constraints(m, p) end - # Calculate total grid cost based on utility grid cost per kW series and net load - if !isempty(p.s.electric_utility.utility_grid_cost_per_kw_series) - calculate_net_load(m, p) - m[:TotalUtilityGridCost] = p.pwf_e * sum(p.s.electric_utility.utility_grid_cost_per_kw_series .* m[:dvNetLoad]) - end - ################################# Objective Function ######################################## @expression(m, Costs, # Capital Costs @@ -545,10 +538,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:AvoidedCapexByGHP] - m[:ResidualGHXCapCost] - # Subtract capital expenditures avoided by inclusion of ASHP - m[:AvoidedCapexByASHP] + - - # Add utility grid cost (not incurred by customer) - only nonzero if utility_grid_cost_per_kw_series provided - m[:TotalUtilityGridCost] + m[:AvoidedCapexByASHP] ); if !isempty(p.s.electric_utility.outage_durations) diff --git a/src/core/utils.jl b/src/core/utils.jl index 2b8f654a9..ac4da7b02 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -163,7 +163,6 @@ function dictkeys_tosymbols(d::Dict) "grid_draw_limit_kw_by_time_step", "export_limit_kw_by_time_step", "outage_probabilities", "renewable_energy_fraction_series", - "utility_grid_cost_per_kw_series", "heating_cop_reference", "heating_cf_reference", "heating_reference_temps_degF", From 555708453e2a3de1186628886395ccb96a7c9112 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 5 May 2026 13:11:36 -0600 Subject: [PATCH 49/66] rm grid cost --- src/core/electric_utility.jl | 18 ++++-------------- test/runtests.jl | 11 +++++++---- test/test_with_xpress.jl | 2 ++ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index 20602f160..af1cbb87e 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -140,8 +140,6 @@ struct ElectricUtility net_metering_limit_kw::Real interconnection_limit_kw::Real transmission_limit_kw::Real - utility_grid_cost_per_kw_series::Array{<:Real,1} - function ElectricUtility(; @@ -199,12 +197,9 @@ struct ElectricUtility ### Grid Clean Energy Fraction Inputs ### cambium_cef_metric::String = "cef_load", # Options = ["cef_load", "cef_gen"] # cef_load is the fraction of generation that is clean, for the generation that is allocated to a region’s end-use load; cef_gen is the fraction of generation that is clean within a region - renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[], # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) - - # Timeseries grid cost input - utility_grid_cost_per_kw_series::Array{<:Real,1} = Float64[] # Timeseries of per kW grid costs (incurred by utility, not the site). - ) - + renewable_energy_fraction_series::Union{Real,Array{<:Real,1}} = Float64[] # Fraction of energy supplied by the grid that is renewable. Can be scalar or timeseries (aligned with time_steps_per_hour) + ) + is_MPC = isnothing(latitude) || isnothing(longitude) cambium_region = "NA - Cambium data not used" # will be overwritten if Cambium is used @@ -350,10 +345,6 @@ struct ElectricUtility throw(@error("Sum of ElectricUtility inputs outage_probabilities must be equal to 1")) end - if !isempty(utility_grid_cost_per_kw_series) && length(utility_grid_cost_per_kw_series) != 8760*time_steps_per_hour - throw(@error("Length of utility_grid_cost_per_kw_series must be $(8760*time_steps_per_hour).")) - end - new( is_MPC ? "" : avert_emissions_region, is_MPC || isnothing(meters_to_region) ? typemax(Int64) : meters_to_region, @@ -377,8 +368,7 @@ struct ElectricUtility scenarios, net_metering_limit_kw, interconnection_limit_kw, - transmission_limit_kw, - utility_grid_cost_per_kw_series + transmission_limit_kw ) end end diff --git a/test/runtests.jl b/test/runtests.jl index 29d80117e..37e7672a9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -993,7 +993,7 @@ else # run HiGHS tests end @testset verbose=true "Net Metering Limit and Wholesale" begin - #case 1: net metering limit is met by PV + # Case 1: net metering limit is met by PV d = JSON.parsefile("./scenarios/net_metering.json") m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) @@ -1002,7 +1002,7 @@ else # run HiGHS tests empty!(m) GC.gc() - #case 2: wholesale rate is high, big-M is met + # Case 2: wholesale rate is high, big-M is met d["ElectricTariff"]["wholesale_rate"] = 5.0 d["PV"]["can_wholesale"] = true m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) @@ -1012,7 +1012,7 @@ else # run HiGHS tests empty!(m) GC.gc() - #case 3: net metering limit is exceeded, no WHL, and min RE % + # Case 3: net metering limit is exceeded, no WHL, and min RE % d["ElectricTariff"]["wholesale_rate"] = 0 d["PV"]["min_kw"] = 50 d["Site"]["renewable_electricity_min_fraction"] = 0.35 @@ -1022,7 +1022,10 @@ else # run HiGHS tests @test results["ElectricTariff"]["lifecycle_export_benefit_after_tax"] ≈ 0.0 atol=1e-3 finalize(backend(m)) empty!(m) - GC.gc() + GC.gc() + + # Case 4: ElectricStorage can net meter + #TODO: add end @testset "Heating loads and addressable load fraction" begin diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index b9847d7ff..8bd16498a 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -260,6 +260,7 @@ end empty!(m2) GC.gc() end +end @testset "FlexibleHVAC" begin @@ -417,6 +418,7 @@ end # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false # @test r["Financial"]["npv"] == 0 # end + end #= From 5062cea9317a852a3dbb4b2837588f35df9a1f52 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 2 Jun 2026 14:31:05 -0600 Subject: [PATCH 50/66] fix NEM issue --- src/constraints/electric_utility_constraints.jl | 4 ++-- src/core/generator.jl | 4 ++-- src/mpc/structs.jl | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 2eb0493ed..054b20495 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -84,8 +84,8 @@ function add_export_constraints(m, p; _n="") #leverage max system sizes for interconnect limit size, alternate is max monthly fully-electrified load in kWh #assume electric heater with COP of 1 for conversion of heat to electricity max_interconnection_size = minimum([ - p.s.electric_utility.interconnection_limit_kw, - sum(p.max_sizes[t] for t in NEM_techs), + p.s.electric_utility.interconnection_limit_kw, + isempty(NEM_techs) ? Inf : sum(p.max_sizes[t] for t in NEM_techs), p.hours_per_time_step * maximum([sum(( p.s.electric_load.loads_kw[ts] + p.s.cooling_load.loads_kw_thermal[ts]/p.cooling_cop["ExistingChiller"][ts] + diff --git a/src/core/generator.jl b/src/core/generator.jl index c9bec8e9f..7b116647d 100644 --- a/src/core/generator.jl +++ b/src/core/generator.jl @@ -19,7 +19,7 @@ sells_energy_back_to_grid::Bool = false, can_net_meter::Bool = false, can_wholesale::Bool = false, - can_export_beyond_nem_limit = false, + can_export_beyond_nem_limit::Bool = false, can_curtail::Bool = false, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, @@ -116,7 +116,7 @@ struct Generator <: AbstractGenerator sells_energy_back_to_grid::Bool = false, can_net_meter::Bool = false, can_wholesale::Bool = false, - can_export_beyond_nem_limit = false, + can_export_beyond_nem_limit::Bool = false, can_curtail::Bool = false, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index 5e3aacfae..ceb665555 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -228,7 +228,7 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage can_grid_charge::Bool = true can_net_meter::Bool = true can_wholesale::Bool = false - can_export_beyond_nem_limit = false + can_export_beyond_nem_limit::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing fixed_soc_series_fraction_tolerance::Union{Nothing, Real} = !isnothing(fixed_soc_series_fraction) ? 0.02 : nothing @@ -245,7 +245,7 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage can_grid_charge::Bool = true can_net_meter::Bool = true can_wholesale::Bool = false - can_export_beyond_nem_limit = false + can_export_beyond_nem_limit::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 max_kw::Float64 = size_kw max_kwh::Float64 = size_kwh From 1ffa3de6c3b9baa5d7f7e6bb920ab18e6e22de5a Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 8 Jun 2026 13:00:01 -0600 Subject: [PATCH 51/66] add BESS export to transmission limit --- src/constraints/electric_utility_constraints.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 054b20495..d9d6c5f27 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -4,6 +4,7 @@ function add_export_constraints(m, p; _n="") ## Imports and Exports must be no greater than the transmission limit m[Symbol("ImportExportLimitCon"*_n)] = @constraint(m, [ts in p.time_steps_with_grid], sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) + + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) + sum(sum( m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers)) <= p.s.electric_utility.transmission_limit_kw ) From 3e5552e9e3258d4ec27233f0b4802b909f07e37e Mon Sep 17 00:00:00 2001 From: adfarth Date: Mon, 8 Jun 2026 13:14:46 -0600 Subject: [PATCH 52/66] add warning about onsite RE --- src/core/energy_storage/electric_storage.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 9371f881d..8154ce018 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -320,6 +320,10 @@ struct ElectricStorage <: AbstractElectricStorage @warn "Battery replacement costs (per_kwh) will not be considered because battery_replacement_year is greater than or equal to analysis_years." end + if !s.include_exported_renewable_electricity_in_total && (stor.can_net_meter || stor.can_wholesale) + @warn "include_exported_renewable_electricity_in_total = false, but ElectricStorage can_net_meter or can_wholesale is true. REopt's calculation of onsite renewable electricity does not currently accurately account for exported renewable electricity via the battery and will thus overestimate onsite renewable electricity." + end +`` can_net_meter = stor.can_net_meter can_wholesale = stor.can_wholesale can_export_beyond_nem_limit = stor.can_export_beyond_nem_limit From f9828620013a90ff5cf948219a697784452b03a4 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 9 Jun 2026 12:37:49 -0600 Subject: [PATCH 53/66] rmv net load calc --- src/constraints/renewable_energy_constraints.jl | 6 ++---- src/core/reopt.jl | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/constraints/renewable_energy_constraints.jl b/src/constraints/renewable_energy_constraints.jl index e1a13e7d4..ac97d6294 100644 --- a/src/constraints/renewable_energy_constraints.jl +++ b/src/constraints/renewable_energy_constraints.jl @@ -79,7 +79,7 @@ function add_re_elec_calcs(m,p) ) # TODO: battery can now discharge to the grid, but this export is not yet accounted for in the RE calculation. # This is only relevant if include_exported_renewable_electricity_in_total = false. If false, the - # battery could become a "back door" for export of RE to the grid since it's not subtracted here. Will require tracking of RE in BESS. + # battery could become a "back door" for export of RE to the grid since it's not subtracted here. Will require tracking of onsite vs grid RE in BESS. ) # + SteamTurbineAnnualREEleckWh # SteamTurbine RE Elec, already adjusted for p.hours_per_time_step ) @@ -94,9 +94,7 @@ function add_re_elec_calcs(m,p) for ts in p.time_steps, b in p.s.storage.types.elec ) # TODO: battery can now discharge to the grid, but this export is not yet accounted for in the RE calculation. - # Need to subtract out *grid RE* discharged from battery back to grid so that loop doesn't become a back door for increasing RE. This will require some careful thought! - - # TODO: Should this subtract out exported energy * renewable_energy_fraction_series? + # Need to subtract out *grid RE* discharged from battery back to grid so that loop doesn't become a back door for increasing RE. Will require tracking of onsite vs grid RE in BESS. ) ) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 31fedcf64..9eadc5aaa 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -659,7 +659,6 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) dvPeakDemandMonth[p.months, 1:p.s.electric_tariff.n_monthly_demand_tiers] >= 0 # Peak electrical power demand during month m [kW] MinChargeAdder >= 0 binGHP[p.ghp_options], Bin # Can be <= 1 if require_ghp_purchase=0, and is ==1 if require_ghp_purchase=1 - dvNetLoad[p.time_steps] # Net load after on-site generation and storage [kW] end if !isempty(p.s.storage.types.elec) From 1280413febc07ff5cddfa571d65744e77f118b1b Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 9 Jun 2026 12:39:02 -0600 Subject: [PATCH 54/66] Update load_balance.jl --- src/constraints/load_balance.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 3520184ed..37174d5c3 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -158,15 +158,6 @@ function add_thermal_load_constraints(m, p; _n="") end end -function calculate_net_load(m, p; _n="") - # Calculate net load (grid purchase - production to grid - BESS to grid) - @constraint(m, [ts in p.time_steps], - m[Symbol("dvNetLoad"*_n)][ts] == sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) - - sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for t in setdiff(p.techs.elec, p.techs.steam_turbine), u in p.export_bins_by_tech[t]) - - sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for b in p.s.storage.types.elec, u in p.export_bins_by_storage[b]) - ) -end - function add_absorption_chiller_load_constraints(m,p;_n="") if !isempty(p.techs.absorption_chiller) for q in p.heating_loads From d2c346b1891ea3a9b2a5ba62cb0a797662e799e1 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 9 Jun 2026 14:16:00 -0600 Subject: [PATCH 55/66] fix WHL max_ben --- CHANGELOG.md | 3 ++- src/constraints/electric_utility_constraints.jl | 2 +- src/core/reopt_inputs.jl | 1 + test/runtests.jl | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4593895..d7925e70a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,8 @@ Classify the change according to the following categories: ## bess-export ### Added -- Add decision variable **dvStorageToGrid** and **ElectricStorage** input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit` +- Allow **ElectricStorage** to export to the grid with input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit`and associated decision variable **dvStorageToGrid** +- Add result **ElectricStorage** `storage_to_grid_series_kw` ## v0.59.2 ### Fixed diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index d9d6c5f27..bd787c8be 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -183,7 +183,7 @@ function add_export_constraints(m, p; _n="") else binWHL = @variable(m, binary = true) @warn "Adding binary variable for wholesale export choice. Some solvers are slow with binaries." - max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:WHL])])*p.pwf_e*p.hours_per_time_step*1000 + 1e8 + max_bene = sum([ld*rate for (ld,rate) in zip(p.s.electric_load.loads_kw, p.s.electric_tariff.export_rates[:WHL])])*p.pwf_e*p.hours_per_time_step*1000 - 1e8 WHL_benefit = @variable(m, lower_bound = max_bene) @constraint(m, binNEM + binWHL == 1) # can either NEM or WHL export, not both diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 6e22457ce..7b2450d77 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -1304,6 +1304,7 @@ function fillin_techs_by_exportbin(techs_by_exportbin::Dict, tech::AbstractTech, end function fillin_storage_by_exportbin(s::AbstractScenario, storage_by_exportbin::Dict, b::String) + # TODO: Consider modifying this function's args to align with fillin_techs_by_exportbin (passing in storage struct instead of scenario) if s.storage.attr[b].can_net_meter && :NEM in keys(storage_by_exportbin) push!(storage_by_exportbin[:NEM], b) if s.storage.attr[b].can_export_beyond_nem_limit && :EXC in keys(storage_by_exportbin) diff --git a/test/runtests.jl b/test/runtests.jl index fc6f03e0f..6506ca856 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -344,7 +344,6 @@ else # run HiGHS tests @test response["chp_max_size_kw"] ≈ case3_max_size_kw atol=0.1 end @testset "January Export Rates" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) data = JSON.parsefile("./scenarios/monthly_rate.json") # 1) create wholesale_rate with compensation in January > retail rate From b099bb3e196fa3b71f9ff0621fe675af50139bd2 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 9 Jun 2026 15:06:07 -0600 Subject: [PATCH 56/66] Update electric_utility_constraints.jl Fixes issue in which WHL and NEM were both being used --- src/constraints/electric_utility_constraints.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index bd787c8be..26fe614a3 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -198,6 +198,12 @@ function add_export_constraints(m, p; _n="") } ) @constraint(m, !binWHL => {WHL_benefit >= 0}) + @constraint(m, [ts in p.time_steps_with_grid, t in p.techs_by_exportbin[:WHL]], + !binWHL => {m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] == 0} + ) + @constraint(m, [ts in p.time_steps_with_grid, b in p.storage_by_exportbin[:WHL]], + !binWHL => {m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] == 0} + ) else @constraint(m, WHL_benefit >= p.pwf_e * p.hours_per_time_step * @@ -208,6 +214,12 @@ function add_export_constraints(m, p; _n="") ) ) @constraint(m, WHL_benefit >= max_bene * binWHL) + @constraint(m, [ts in p.time_steps_with_grid, t in p.techs_by_exportbin[:WHL]], + m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] <= binWHL * sum(p.s.electric_load.loads_kw) + ) + @constraint(m, [ts in p.time_steps_with_grid, b in p.storage_by_exportbin[:WHL]], + m[Symbol("dvStorageToGrid"*_n)][b, :WHL, ts] <= binWHL * sum(p.s.electric_load.loads_kw) + ) end end end From 9e1d300d7bb8f4eceba7d6b908b232b3fd9c1147 Mon Sep 17 00:00:00 2001 From: adfarth Date: Tue, 9 Jun 2026 16:07:42 -0600 Subject: [PATCH 57/66] add tests --- CHANGELOG.md | 4 +++- test/runtests.jl | 55 ++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7925e70a..0ce049ba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,9 @@ Classify the change according to the following categories: ## bess-export ### Added - Allow **ElectricStorage** to export to the grid with input options for grid export: `can_net_meter`, `can_wholesale`, `can_export_beyond_nem_limit`and associated decision variable **dvStorageToGrid** -- Add result **ElectricStorage** `storage_to_grid_series_kw` +- Add result **ElectricStorage** `storage_to_grid_series_kw` +### Fixed +- Bug in max benefit constraint for WHL export; bug allowing for both WHL and NEM to be used ## v0.59.2 ### Fixed diff --git a/test/runtests.jl b/test/runtests.jl index 6506ca856..950e0b2b9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1023,8 +1023,59 @@ else # run HiGHS tests empty!(m) GC.gc() - # Case 4: ElectricStorage can net meter - #TODO: add + # Case 4: ElectricStorage can net meter and energy rate is lower during PV production + # expect battery to do energy arbitrage and NEM export during higher rate + d = JSON.parsefile("./scenarios/bess_export.json") + d["ElectricStorage"]["min_kw"] = 50 + d["ElectricStorage"]["max_kw"] = 50 + d["ElectricStorage"]["min_kwh"] = 50*6 + d["ElectricStorage"]["max_kwh"] = 50*6 + d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.5, 0.1, 0.5], inner=8, outer=365) + p = REoptInputs(d) + for exbin in [:WHL, :NEM] + @test exbin in p.export_bins_by_storage["ElectricStorage"] + end + @test !(:EXC in p.export_bins_by_storage["ElectricStorage"]) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.02)) + results = run_reopt(m, p) + @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 + @test all(x == 0.0 for (i,x) in enumerate(results["ElectricStorage"]["storage_to_grid_series_kw"]) if 8 < i % 24 < 17) + @test value(m[:NEM_benefit]) <= 0 + @test value(m[:WHL_benefit]) ≈ 0.0 atol=1e-5 + @test value(m[:EXC_benefit]) ≈ 0.0 atol=1e-5 + @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 + finalize(backend(m)) + empty!(m) + GC.gc() + + # Case 5: ElectricStorage wholesale + d["ElectricUtility"]["net_metering_limit_kw"] = 0 + # Set wholesale rate to 0.01 from 6am-8pm and 0.15 otherwise; repeat array for 1 year + wholesale_rate = [6 <= hour < 20 ? 0.01 : 0.15 for hour in 0:23] + d["ElectricTariff"]["wholesale_rate"] = repeat(wholesale_rate, 365) + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.18 + d["ElectricTariff"]["blended_annual_demand_rate"] = 0.0 + + # Fix sizes + d["PV"]["min_kw"] = 200 + d["PV"]["max_kw"] = 200 + d["ElectricStorage"]["min_kw"] = 50 + d["ElectricStorage"]["max_kw"] = 50 + d["ElectricStorage"]["min_kwh"] = 200 + d["ElectricStorage"]["max_kwh"] = 200 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + + @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 + @test value(m[:NEM_benefit]) ≈ 0.0 atol=1e-5 + @test value(m[:WHL_benefit]) <= 0 + @test value(m[:EXC_benefit]) ≈ 0.0 atol=1e-5 + @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 + finalize(backend(m)) + empty!(m) + GC.gc() + end @testset "Heating loads and addressable load fraction" begin From 96df208a82bed27695dc847fc937b101a07f4c65 Mon Sep 17 00:00:00 2001 From: adfarth Date: Wed, 10 Jun 2026 08:36:57 -0600 Subject: [PATCH 58/66] comment out development_tests --- test/development_tests.jl | 143 ++++++++++++++++++++------------------ 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/test/development_tests.jl b/test/development_tests.jl index 4a0563e99..e6e20f884 100644 --- a/test/development_tests.jl +++ b/test/development_tests.jl @@ -1,72 +1,79 @@ -@testset verbose=true "Battery Can Export" begin +""" +This scratch space can be used for development of new tests. Delete tests that are not longer needed. + Run these tests with: + using Pkg + Pkg.test("REopt"; test_args=["Dev"]) +""" - # case where whl rate is between off peak and on peak retail rates? - # or whl rate > retail rate and allow_simultaneous_export_import = false (copy existing test) +# @testset verbose=true "Battery Can Export" begin - # Case 1: energy rate is lower during PV production - # so expect battery to do energy arbitrage and NEM export during higher rate - d = JSON.parsefile("./scenarios/bess_export.json") - d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.5, 0.1, 0.5], inner=8, outer=365) - # d["ElectricStorage"]["can_net_meter"] = true - # d["ElectricStorage"]["can_wholesale"] = true - # d["PV"]["can_net_meter"] = false - # d["PV"]["can_wholesale"] = false - p = REoptInputs(d) - for exbin in [:WHL, :NEM] - @test exbin in p.export_bins_by_storage["ElectricStorage"] - end - @test !(:EXC in p.export_bins_by_storage["ElectricStorage"]) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - open("debug_results.json","w") do f - JSON.print(f, results, 4) - end - # @test results["PV"]["electric_to_grid_series_kw"] - @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 - @test all(x == 0.0 for (i,x) in enumerate(results["ElectricStorage"]["storage_to_grid_series_kw"]) if 8 < i % 24 < 17) - @test value(m[:NEM_benefit]) <= 0 - @test value(m[:WHL_benefit]) == 0 - @test value(m[:EXC_benefit]) == 0 - @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 - finalize(backend(m)) - empty!(m) - GC.gc() +# # case where whl rate is between off peak and on peak retail rates? +# # or whl rate > retail rate and allow_simultaneous_export_import = false (copy existing test) - # Case 2: whl rate > retail rate and allow_simultaneous_export_import = false - # so expect battery to wholesale once load is met - #TODO: have to limit PV size to get expected result? - d["ElectricStorage"]["can_net_meter"] = false - d["PV"]["can_net_meter"] = false - d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.1], 8760) - d["ElectricTariff"]["wholesale_rate"] = append!(repeat([5], 31*24), repeat([0], 8760 - 31*24)) - d["ElectricUtility"]["allow_simultaneous_export_import"] = false - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - open("debug_results2.json","w") do f - JSON.print(f, results, 4) - end - @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"]) - if results["ElectricStorage"]["storage_to_grid_series_kw"][i] > 0) - @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 - @test value(m[:NEM_benefit]) == 0 - @test value(m[:EXC_benefit]) == 0 - @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 - finalize(backend(m)) - empty!(m) - GC.gc() +# # Case 1: energy rate is lower during PV production +# # so expect battery to do energy arbitrage and NEM export during higher rate +# d = JSON.parsefile("./scenarios/bess_export.json") +# d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.5, 0.1, 0.5], inner=8, outer=365) +# # d["ElectricStorage"]["can_net_meter"] = true +# # d["ElectricStorage"]["can_wholesale"] = true +# # d["PV"]["can_net_meter"] = false +# # d["PV"]["can_wholesale"] = false +# p = REoptInputs(d) +# for exbin in [:WHL, :NEM] +# @test exbin in p.export_bins_by_storage["ElectricStorage"] +# end +# @test !(:EXC in p.export_bins_by_storage["ElectricStorage"]) +# m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) +# results = run_reopt(m, p) +# open("debug_results.json","w") do f +# JSON.print(f, results, 4) +# end +# # @test results["PV"]["electric_to_grid_series_kw"] +# @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 +# @test all(x == 0.0 for (i,x) in enumerate(results["ElectricStorage"]["storage_to_grid_series_kw"]) if 8 < i % 24 < 17) +# @test value(m[:NEM_benefit]) <= 0 +# @test value(m[:WHL_benefit]) ≈ 0.0 atol=1e-5 +# @test value(m[:EXC_benefit]) ≈ 0.0 atol=1e-5 +# @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 +# finalize(backend(m)) +# empty!(m) +# GC.gc() - # More testing of storage export inputs - # d["ElectricStorage"]["can_net_meter"] = true - # d["ElectricStorage"]["can_wholesale"] = true - d["ElectricStorage"]["can_export_beyond_nem_limit"] = true - p = REoptInputs(d) - println(p.export_bins_by_storage["ElectricStorage"]) - for exbin in [:EXC, :WHL, :NEM] - @test exbin in p.export_bins_by_storage["ElectricStorage"] - end - d["ElectricStorage"]["can_net_meter"] = false - d["ElectricStorage"]["can_wholesale"] = false - d["ElectricStorage"]["can_export_beyond_nem_limit"] = false - p = REoptInputs(d) - @test isempty(p.export_bins_by_storage["ElectricStorage"]) -end +# # Case 2: whl rate > retail rate and allow_simultaneous_export_import = false +# # so expect battery to wholesale once load is met +# #TODO: have to limit PV size to get expected result? +# d["ElectricStorage"]["can_net_meter"] = false +# d["PV"]["can_net_meter"] = false +# d["ElectricTariff"]["tou_energy_rates_per_kwh"] = repeat([0.1], 8760) +# d["ElectricTariff"]["wholesale_rate"] = append!(repeat([5], 31*24), repeat([0], 8760 - 31*24)) +# d["ElectricUtility"]["allow_simultaneous_export_import"] = false +# m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) +# results = run_reopt(m, d) +# open("debug_results2.json","w") do f +# JSON.print(f, results, 4) +# end +# @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"]) +# if results["ElectricStorage"]["storage_to_grid_series_kw"][i] > 0) +# @test sum(results["ElectricStorage"]["storage_to_grid_series_kw"]) > 0 +# @test value(m[:NEM_benefit]) == 0 +# @test value(m[:EXC_benefit]) == 0 +# @test results["ElectricTariff"]["year_one_export_benefit_before_tax"] >= 0 +# finalize(backend(m)) +# empty!(m) +# GC.gc() + +# # More testing of storage export inputs +# # d["ElectricStorage"]["can_net_meter"] = true +# # d["ElectricStorage"]["can_wholesale"] = true +# d["ElectricStorage"]["can_export_beyond_nem_limit"] = true +# p = REoptInputs(d) +# println(p.export_bins_by_storage["ElectricStorage"]) +# for exbin in [:EXC, :WHL, :NEM] +# @test exbin in p.export_bins_by_storage["ElectricStorage"] +# end +# d["ElectricStorage"]["can_net_meter"] = false +# d["ElectricStorage"]["can_wholesale"] = false +# d["ElectricStorage"]["can_export_beyond_nem_limit"] = false +# p = REoptInputs(d) +# @test isempty(p.export_bins_by_storage["ElectricStorage"]) +# end From 88a0f432a7fdf9967c76f7057183e8ac686eaae5 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 9 Jul 2026 15:22:06 -0600 Subject: [PATCH 59/66] Update electric_utility_constraints.jl --- src/constraints/electric_utility_constraints.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index 26fe614a3..b2c8f07c7 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -445,8 +445,8 @@ end function add_elec_utility_expressions(m, p; _n="") - if !isempty(p.s.electric_tariff.export_bins) && !isempty(p.techs.all) - # NOTE: levelization_factor is baked into dvProductionToGrid and dvStorageToGrid + if !isempty(p.s.electric_tariff.export_bins) && (!isempty(p.techs.all) || !isempty(p.s.storage.types.elec)) + # NOTE: levelization_factor is baked into dvProductionToGrid and dvStorageToGrid m[Symbol("TotalExportBenefit"*_n)] = m[Symbol("NEM_benefit"*_n)] + m[Symbol("WHL_benefit"*_n)] + m[Symbol("EXC_benefit"*_n)] else From 63601f0ec1204a539266fea6b614fb71fb7b545e Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 9 Jul 2026 15:46:55 -0600 Subject: [PATCH 60/66] rmv ununsed units --- src/core/energy_storage/electric_storage.jl | 2 +- src/mpc/structs.jl | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 8154ce018..2bf60cff8 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -323,7 +323,7 @@ struct ElectricStorage <: AbstractElectricStorage if !s.include_exported_renewable_electricity_in_total && (stor.can_net_meter || stor.can_wholesale) @warn "include_exported_renewable_electricity_in_total = false, but ElectricStorage can_net_meter or can_wholesale is true. REopt's calculation of onsite renewable electricity does not currently accurately account for exported renewable electricity via the battery and will thus overestimate onsite renewable electricity." end -`` + can_net_meter = stor.can_net_meter can_wholesale = stor.can_wholesale can_export_beyond_nem_limit = stor.can_export_beyond_nem_limit diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index ceb665555..11880522b 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -226,9 +226,8 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - can_net_meter::Bool = true - can_wholesale::Bool = false - can_export_beyond_nem_limit::Bool = false + # can_net_meter::Bool = false + # can_wholesale::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing fixed_soc_series_fraction_tolerance::Union{Nothing, Real} = !isnothing(fixed_soc_series_fraction) ? 0.02 : nothing @@ -243,9 +242,9 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - can_net_meter::Bool = true - can_wholesale::Bool = false - can_export_beyond_nem_limit::Bool = false + # TODO: impement net metering and wholesale in MPC + # can_net_meter::Bool = false + # can_wholesale::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 max_kw::Float64 = size_kw max_kwh::Float64 = size_kwh From 8cb3e9272fe2f8bb15cd98527578990fcd5a4133 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 10 Jul 2026 14:00:11 -0600 Subject: [PATCH 61/66] Update electric_tariff.jl --- src/results/electric_tariff.jl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/results/electric_tariff.jl b/src/results/electric_tariff.jl index 44cb916b7..803ad6c2a 100644 --- a/src/results/electric_tariff.jl +++ b/src/results/electric_tariff.jl @@ -221,22 +221,29 @@ function add_electric_tariff_results(m::JuMP.AbstractModel, p::REoptInputs, d::D binmap[Symbol("WHL")] = "wholesale" binmap[Symbol("NEM")] = "net_metering" binmap[Symbol("EXC")] = "net_metering_excess" - if !isempty(p.techs.elec) + if !isempty(p.s.electric_tariff.export_bins) && (!isempty(p.techs.elec) || !isempty(p.s.storage.types.elec)) for bin in p.s.electric_tariff.export_bins rate_series = string(binmap[bin], "_export_rate_series") export_series = string(binmap[bin], "_electric_to_grid_series_kw") r[rate_series] = collect(p.s.electric_tariff.export_rates[bin]) - r[export_series] = collect(value.(sum(m[Symbol("dvProductionToGrid"*_n)][t, bin, :] for t in p.techs.elec))) + + r[export_series] = collect(value.([ + sum((m[Symbol("dvProductionToGrid"*_n)][t, bin, ts] for t in p.techs.elec); init=0.0) + + sum((m[Symbol("dvStorageToGrid"*_n)][b, bin, ts] for b in p.s.storage.types.elec); init=0.0) + for ts in p.time_steps + ])) r[string(binmap[bin], "_monthly_export_series_kwh")] = [] r[string(binmap[bin], "_monthly_export_cost_benefit_before_tax")] = [] for mth in 1:12 idx = findall(x -> Dates.month(x) == mth, dr) - push!(r[string(binmap[bin], "_monthly_export_series_kwh")], sum(r[export_series][idx]) / p.s.time_steps_per_hour) - push!(r[string(binmap[bin], "_monthly_export_cost_benefit_before_tax")], sum((r[rate_series].*r[export_series])[idx]) / p.s.time_steps_per_hour) + push!(r[string(binmap[bin], "_monthly_export_series_kwh")], sum(r[export_series][idx]) / p.s.settings.time_steps_per_hour) + push!(r[string(binmap[bin], "_monthly_export_cost_benefit_before_tax")], sum((r[rate_series].*r[export_series])[idx]) / p.s.settings.time_steps_per_hour) end end + else + @info "No electricity export compensation timeseries to include in ElectricTariff results." end end From c5a452c160bdc0b3fc7c6a92b9f1ee3c69c9c151 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 10 Jul 2026 14:06:59 -0600 Subject: [PATCH 62/66] Update constraints.jl --- src/mpc/constraints.jl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/mpc/constraints.jl b/src/mpc/constraints.jl index ee8127f9c..8f0a156e7 100644 --- a/src/mpc/constraints.jl +++ b/src/mpc/constraints.jl @@ -24,12 +24,17 @@ function add_grid_draw_limits(m::JuMP.AbstractModel, p::MPCInputs; _n="") ) end - function add_export_limits(m::JuMP.AbstractModel, p::MPCInputs; _n="") @constraint(m, [ts in p.time_steps], sum( - sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for u in p.export_bins_by_tech[t]) - for t in p.techs.elec + sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for u in p.export_bins_by_tech[t]; init=0.0) + for t in p.techs.elec; + init=0.0 + ) + + sum( + sum(m[Symbol("dvStorageToGrid"*_n)][b, u, ts] for u in p.export_bins_by_storage[b]; init=0.0) + for b in p.s.storage.types.elec; + init=0.0 ) <= p.s.limits.export_limit_kw_by_time_step[ts] ) -end +end \ No newline at end of file From 3e53764eb3656522ce06d11dad22628e889f57f0 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 10 Jul 2026 14:08:40 -0600 Subject: [PATCH 63/66] Update structs.jl --- src/mpc/structs.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index 11880522b..b32e1ed00 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -226,8 +226,8 @@ Base.@kwdef struct MPCElectricStorage < AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - # can_net_meter::Bool = false - # can_wholesale::Bool = false + can_net_meter::Bool = false + can_wholesale::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 fixed_soc_series_fraction::Union{Nothing, Array{<:Real,1}} = nothing fixed_soc_series_fraction_tolerance::Union{Nothing, Real} = !isnothing(fixed_soc_series_fraction) ? 0.02 : nothing @@ -242,9 +242,9 @@ Base.@kwdef struct MPCElectricStorage <: AbstractElectricStorage soc_min_fraction::Float64 = 0.2 soc_init_fraction::Float64 = 0.5 can_grid_charge::Bool = true - # TODO: impement net metering and wholesale in MPC - # can_net_meter::Bool = false - # can_wholesale::Bool = false + # TODO: implement net metering and wholesale in MPC + can_net_meter::Bool = false + can_wholesale::Bool = false grid_charge_efficiency::Float64 = 0.96 * 0.975^2 max_kw::Float64 = size_kw max_kwh::Float64 = size_kwh From d6014ca9dcb7a97e4b670d7a8da310256a9d79fd Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 10 Jul 2026 16:04:27 -0600 Subject: [PATCH 64/66] Update electric_utility_constraints.jl --- src/constraints/electric_utility_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index b2c8f07c7..726bb0614 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -445,7 +445,7 @@ end function add_elec_utility_expressions(m, p; _n="") - if !isempty(p.s.electric_tariff.export_bins) && (!isempty(p.techs.all) || !isempty(p.s.storage.types.elec)) + if !isempty(p.s.electric_tariff.export_bins) && (!isempty(p.techs.elec) || !isempty(p.s.storage.types.elec)) # NOTE: levelization_factor is baked into dvProductionToGrid and dvStorageToGrid m[Symbol("TotalExportBenefit"*_n)] = m[Symbol("NEM_benefit"*_n)] + m[Symbol("WHL_benefit"*_n)] + m[Symbol("EXC_benefit"*_n)] From 9daa25d60825e81fe66f89f00808bd609d2de14e Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 16 Jul 2026 11:28:22 -0600 Subject: [PATCH 65/66] update cst test values --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 950e0b2b9..6ea052828 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3798,11 +3798,11 @@ else # run HiGHS tests p = REoptInputs(s) m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) results = run_reopt(m1, p) - @test results["CST"]["annual_thermal_production_mmbtu"] ≈ 6626.9 rtol=1.0e-3 - @test sum(results["CST"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 19548.3 rtol=1.0e-3 + @test results["CST"]["annual_thermal_production_mmbtu"] ≈ 6656.36 rtol=1.0e-3 + @test sum(results["CST"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 19708.99 rtol=1.0e-3 @test results["CST"]["size_kw"] ≈ 10000.0 atol=0.001 @test results["CST"]["size_mmbtu_per_hour"] ≈ 10000.0 / REopt.KWH_PER_MMBTU atol=1.0e-3 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 386.4 rtol=1.0e-3 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 356.56 rtol=1.0e-3 @test results["HighTempThermalStorage"]["size_kwh"] ≈ 10000.0 atol=0.1 @test results["HotThermalStorage"]["size_gal"] ≈ 1000.0 atol=0.1 From c9305d46e8e9e58876fa9073e33b90e8e6897e33 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 16 Jul 2026 11:37:18 -0600 Subject: [PATCH 66/66] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 2bf60cff8..781564a9c 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -178,7 +178,7 @@ end can_grid_charge::Bool = off_grid_flag ? false : true can_net_meter::Bool = false can_wholesale::Bool = false - can_export_beyond_nem_limit = false + can_export_beyond_nem_limit::Bool = false installed_cost_per_kw::Real = 968.0 # Cost of power components (e.g., inverter and BOS) installed_cost_per_kwh::Real = 253.0 # Cost of energy components (e.g., battery pack) installed_cost_constant::Real = 222115.0 # "+c" constant cost that is added to total ElectricStorage installed costs if a battery is included. Accounts for costs not expected to scale with power or energy capacity. @@ -224,7 +224,7 @@ Base.@kwdef struct ElectricStorageDefaults can_grid_charge::Bool = off_grid_flag ? false : true can_net_meter::Bool = false can_wholesale::Bool = false - can_export_beyond_nem_limit = false + can_export_beyond_nem_limit::Bool = false installed_cost_per_kw::Real = 968.0 installed_cost_per_kwh::Real = 253.0 installed_cost_constant::Real = 222115.0