Allow BESS to Export#471
Conversation
| 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]] |
There was a problem hiding this comment.
This isn't really about what you added, but isn't NEM_techs just equal to techs_by_exportbins[:NEM]? Same with WHL_techs, NEM_storage, WHL_storage. Do you agree? If so idk why we are recreating it.
There was a problem hiding this comment.
Yes I just double checked and I agree. We could probably replace all instances of NEM_techs and WHL_techs with their techs_by_export bin equivalent
There was a problem hiding this comment.
@hdunham it looks like you made this change and then reverted it. I'm assuming it broke some things and if so can I resolve this convo?
| @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 ? |
There was a problem hiding this comment.
no discharge_efficiency here, that is applied in the SOC constraints (4g)
There was a problem hiding this comment.
To be sure I understand, is dvProductionToStorage on the AC side of the converter (so, before efficiency losses) and dvDischargeFromStorage is on the AC side of the inverter (so, after efficiency losses)? So everything is AC
| 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) |
There was a problem hiding this comment.
@hdunham how does this suggestion differ from export_bins_by_tech?
zolanaj
left a comment
There was a problem hiding this comment.
Approval of big-M implementation
this is all now in cost-to-utility branch
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 22 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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 |
| 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 |
| # d["ElectricStorage"]["can_wholesale"] = true | ||
| d["ElectricStorage"]["can_export_beyond_nem_limit"] = true | ||
| p = REoptInputs(d) | ||
| println(p.export_bins_by_storage["ElectricStorage"]) |
| @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") | ||
|
|
||
| # 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) |
| 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. | ||
| ) | ||
| # 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. | ||
| ) |
| 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) | ||
| 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) | ||
| ) |
| 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 |
Added
can_net_meter,can_wholesale,can_export_beyond_nem_limit(all default to False)TODO:
dvStorageToGridon metering type, following the logic of tech export binsdvDischargeFromStorageas total discharge withdvStorageToGriddvStorageToGridinto load balancing constraintsThings to test:
Known issues:
net_metering_limit_kw> 0,PV.can_net_meter = false, andElectricStorage.can_net_meter = true, will error --> Could maybe add a check that not only BESS can net meter without a generating tech? Or just allow this?ElectricStoragecan end up being sized, and export, much more than the peak load (this is currently true in develop, too, but now the issue is more extreme)Nice to haves / future work: