Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add timeseries operating reserve outputs #268

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## v0.33.1
### Added
- Add timeseries operating reserve outputs for each tech for off-grid only
### Changed
- Change units for total timeseries operating reserves required and provided to kW (from kWh)

## v0.33.0
### Added
- Functionality to evaluate scenarios with Wind can in the ERP (`backup_reliability`)
Expand Down
4 changes: 2 additions & 2 deletions src/core/production_factor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ function get_production_factor(pv::PV, latitude::Real, longitude::Real; timefram
return pv.production_factor_series
end

watts, ambient_temp_celcius = call_pvwatts_api(latitude, longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type,
watts, ambient_temp_celcius, pv_station_distance_km = call_pvwatts_api(latitude, longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type,
array_type=pv.array_type, losses=round(pv.losses*100, digits=3), dc_ac_ratio=pv.dc_ac_ratio,
gcr=pv.gcr, inv_eff=pv.inv_eff*100, timeframe=timeframe, radius=pv.radius,
time_steps_per_hour=time_steps_per_hour)

return watts
return watts, pv_station_distance_km

end

Expand Down
8 changes: 4 additions & 4 deletions src/core/reopt_inputs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function REoptInputs(s::AbstractScenario)

time_steps = 1:length(s.electric_load.loads_kw)
hours_per_time_step = 1 / s.settings.time_steps_per_hour
techs, pv_to_location, maxsize_pv_locations, pvlocations,
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,
tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2,
Expand Down Expand Up @@ -349,7 +349,7 @@ function setup_tech_inputs(s::AbstractScenario)
setup_operating_reserve_fraction(s, techs_operating_reserve_req_fraction)
end

return techs, pv_to_location, maxsize_pv_locations, pvlocations,
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,
tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2,
Expand Down Expand Up @@ -435,7 +435,7 @@ end

function setup_pv_inputs(s::AbstractScenario, max_sizes, min_sizes,
existing_sizes, cap_cost_slope, om_cost_per_kw, production_factor,
pvlocations, pv_to_location, maxsize_pv_locations,
pvlocations, pv_to_location, maxsize_pv_locations,
segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint,
techs_by_exportbin, techs)

Expand All @@ -444,7 +444,7 @@ function setup_pv_inputs(s::AbstractScenario, max_sizes, min_sizes,
roof_max_kw, land_max_kw = 1.0e5, 1.0e5

for pv in s.pvs
production_factor[pv.name, :] = get_production_factor(pv, s.site.latitude, s.site.longitude;
production_factor[pv.name, :], pv_station_distance_km = get_production_factor(pv, s.site.latitude, s.site.longitude;
time_steps_per_hour=s.settings.time_steps_per_hour)
for location in pvlocations
if pv.location == String(location) # Must convert symbol to string
Expand Down
4 changes: 2 additions & 2 deletions src/core/scenario.jl
Original file line number Diff line number Diff line change
Expand Up @@ -432,12 +432,12 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
# By assigning pv.production_factor_series here, it will skip the PVWatts call in get_production_factor(PV) call from reopt_input.jl
if !isempty(pvs)
for pv in pvs
pv.production_factor_series, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type,
pv.production_factor_series, ambient_temp_celcius, pv_station_distance_km = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type,
array_type=pv.array_type, losses=round(pv.losses*100, digits=3), dc_ac_ratio=pv.dc_ac_ratio,
gcr=pv.gcr, inv_eff=pv.inv_eff*100, timeframe="hourly", radius=pv.radius, time_steps_per_hour=settings.time_steps_per_hour)
end
else
pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour)
pv_prodfactor, ambient_temp_celcius, pv_station_distance_km = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour)
end
ambient_temp_degF = ambient_temp_celcius * 1.8 .+ 32.0
else
Expand Down
4 changes: 3 additions & 1 deletion src/core/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ end
This calls the PVWatts API and returns both:
- PV production factor
- Ambient outdoor air dry bulb temperature profile [Celcius]
- pv_station_distance_km
"""
function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimuth=180, module_type=0, array_type=1,
losses=14, dc_ac_ratio=1.2, gcr=0.4, inv_eff=96, timeframe="hourly", radius=0, time_steps_per_hour=1)
Expand Down Expand Up @@ -394,6 +395,7 @@ function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimut
# Get both possible data of interest
watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W)
tamb_celcius = collect(get(response["outputs"], "tamb", [])) # Celcius
pv_station_distance_km = response["station_info"]["distance"] / 1000 # Distance between the input location and the climate station. (meters)
# Validate outputs
if length(watts) != 8760
throw(@error("PVWatts did not return a valid prodfactor. Got $watts"))
Expand All @@ -407,7 +409,7 @@ function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimut
watts = repeat(watts, inner=time_steps_per_hour)
tamb_celcius = repeat(tamb_celcius, inner=time_steps_per_hour)
end
return watts, tamb_celcius
return watts, tamb_celcius, pv_station_distance_km
catch e
throw(@error("Error occurred when calling PVWatts: $e"))
end
Expand Down
8 changes: 4 additions & 4 deletions src/results/electric_load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
- `annual_calculated_kwh` sum of the `load_series_kw`
- `offgrid_load_met_series_kw` vector of electric load met by generation techs, for off-grid scenarios only
- `offgrid_load_met_fraction` percentage of total electric load met on an annual basis, for off-grid scenarios only
- `offgrid_annual_oper_res_required_series_kwh` , total operating reserves required (for load and techs) on an annual basis, for off-grid scenarios only
- `offgrid_annual_oper_res_provided_series_kwh` , total operating reserves provided on an annual basis, for off-grid scenarios only
- `offgrid_annual_oper_res_required_series_kw` , total operating reserves required (for load and techs) on an annual basis, for off-grid scenarios only
- `offgrid_annual_oper_res_provided_series_kw` , total operating reserves provided on an annual basis, for off-grid scenarios only

!!! note "'Series' and 'Annual' energy outputs are average annual"
REopt performs load balances using average annual production values for technologies that include degradation.
Expand All @@ -33,8 +33,8 @@ function add_electric_load_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dic
sum(p.s.electric_load.critical_loads_kw))
r["offgrid_load_met_fraction"] = round(value(LoadMetPct), digits=6)

r["offgrid_annual_oper_res_required_series_kwh"] = round.(value.(m[:OpResRequired][ts] for ts in p.time_steps_without_grid), digits=3)
r["offgrid_annual_oper_res_provided_series_kwh"] = round.(value.(m[:OpResProvided][ts] for ts in p.time_steps_without_grid), digits=3)
r["offgrid_annual_oper_res_required_series_kw"] = round.(value.(m[:OpResRequired][ts] for ts in p.time_steps_without_grid), digits=3)
r["offgrid_annual_oper_res_provided_series_kw"] = round.(value.(m[:OpResProvided][ts] for ts in p.time_steps_without_grid), digits=3)
end

d["ElectricLoad"] = r
Expand Down
3 changes: 3 additions & 0 deletions src/results/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::
))
end
end
if p.s.settings.off_grid_flag
r["operating_reserve_provided_series_kw"] = value.(m[Symbol("dvOpResFromBatt"*_n)][b,ts] for ts in p.time_steps)
end
else
r["soc_series_fraction"] = []
r["storage_to_load_series_kw"] = []
Expand Down
9 changes: 8 additions & 1 deletion src/results/generator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `electric_to_grid_series_kw` Vector of power sent to grid in an average year
- `electric_to_load_series_kw` Vector of power sent to load in an average year
- `annual_energy_produced_kwh` Average annual energy produced over analysis period
- `operating_reserve_provided_series_kw` For offgrid analyses: operating reserve provided by generator in each time step

!!! note "'Series' and 'Annual' energy outputs are average annual"
REopt performs load balances using average annual production values for technologies that include degradation.
Expand Down Expand Up @@ -68,6 +69,12 @@ function add_generator_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _
for t in p.techs.gen, ts in p.time_steps)
)
r["annual_energy_produced_kwh"] = round(value(AverageGenProd), digits=0)

if p.s.settings.off_grid_flag
GeneratorOR = @expression(m, [ts in p.time_steps],
sum(m[:dvOpResFromTechs][t,ts] for t in p.techs.gen))
r["operating_reserve_provided_series_kw"] = round.(value.(GeneratorOR), digits=3)
end

d["Generator"] = r
nothing
Expand Down Expand Up @@ -117,7 +124,7 @@ function add_generator_results(m::JuMP.AbstractModel, p::MPCInputs, d::Dict; _n=
for t in p.techs.gen, ts in p.time_steps)
)
r["energy_produced_kwh"] = round(value(Year1GenProd), digits=0)

d["Generator"] = r
nothing
end
4 changes: 4 additions & 0 deletions src/results/pv.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- `electric_curtailed_series_kw` Vector of power curtailed over the first 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
- `operating_reserve_provided_series_kw` For offgrid analyses: operating reserve provided by PV in each time step

!!! warn
The key(s) used to access PV outputs in the results dictionary is determined by the `PV.name` value to allow for modeling multiple PV options. (The default `PV.name` is "PV".)
Expand Down Expand Up @@ -66,6 +67,9 @@ function add_pv_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
PVPerUnitSizeOMCosts = p.om_cost_per_kw[t] * p.pwf_om * m[Symbol("dvSize"*_n)][t]
r["lifecycle_om_cost_after_tax"] = round(value(PVPerUnitSizeOMCosts) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0)
r["lcoe_per_kwh"] = calculate_lcoe(p, r, get_pv_by_name(t, p.s.pvs))
if p.s.settings.off_grid_flag
r["operating_reserve_provided_series_kw"] = value.(m[Symbol("dvOpResFromTechs"*_n)][t,ts] for ts in p.time_steps)
end
d[t] = r
end
nothing
Expand Down
6 changes: 6 additions & 0 deletions src/results/wind.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- `lcoe_per_kwh` Levelized Cost of Energy produced by the PV system
- `electric_curtailed_series_kw` Vector of power curtailed over an average year
- `production_factor_series` Wind production factor in each time step, either provided by user or obtained from SAM
- `operating_reserve_provided_series_kw` For offgrid analyses: operating reserve provided by wind in each time step

!!! note "'Series' and 'Annual' energy outputs are average annual"
REopt performs load balances using average annual production values for technologies that include degradation.
Expand Down Expand Up @@ -66,6 +67,11 @@ function add_wind_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
r["annual_energy_produced_kwh"] = round(value(AvgWindProd), digits=0)

r["lcoe_per_kwh"] = calculate_lcoe(p, r, p.s.wind)

if p.s.settings.off_grid_flag
r["operating_reserve_provided_series_kw"] = value.(m[Symbol("dvOpResFromTechs"*_n)][t,ts] for ts in p.time_steps)
end

d[t] = r
nothing
end
6 changes: 3 additions & 3 deletions test/test_with_xpress.jl
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ end
=#

# Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC
pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431);
pf, tamb, dist = REopt.call_pvwatts_api(30.2672, -97.7431);
R = 0.00025 # K/kW
C = 1e5 # kJ/K
# the starting scenario has flat fuel and electricty costs
Expand Down Expand Up @@ -1075,7 +1075,7 @@ end
# Test outputs
@test r["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0 # no interaction with grid
@test r["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ 2617.092 atol=0.01 # Check straight line depreciation calc
@test sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) # OR provided >= required
@test sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kw"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kw"]) # OR provided >= required
@test r["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction
@test r["PV"]["size_kw"] ≈ 5050.0
f = r["Financial"]
Expand Down Expand Up @@ -1140,7 +1140,7 @@ end

windOR = sum(results["Wind"]["electric_to_load_series_kw"] * post["Wind"]["operating_reserve_required_fraction"])
loadOR = sum(post["ElectricLoad"]["loads_kw"] * scen.electric_load.operating_reserve_required_fraction)
@test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) ≈ loadOR + windOR atol=1.0
@test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kw"]) ≈ loadOR + windOR atol=1.0

end

Expand Down
Loading