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

Sam 391 ssc 825 merchant plant forecast upgrades #1235

Merged
merged 22 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c642e95
First draft discharging constraints based on interconnection limits f…
brtietz Oct 7, 2024
d5e26cd
Merge branch 'develop' of https://github.com/NREL/ssc into sam_181_in…
brtietz Oct 15, 2024
3f53f78
New variable to control ability to charge from curtailed power indepe…
brtietz Oct 18, 2024
23fa7a8
Incorporate curtail charging into powerflow and manual dispatch. Func…
brtietz Oct 18, 2024
a43949f
Add to retail rates dispatch, consider curtailment limits FOM
brtietz Oct 18, 2024
0d07145
Merge branch 'develop' of https://github.com/NREL/ssc into sam_181_in…
brtietz Oct 19, 2024
cdfd79d
Tests for interconnection limits
brtietz Oct 21, 2024
786c3bb
Merge branch 'develop' of https://github.com/NREL/ssc into sam_181_in…
brtietz Oct 21, 2024
eb2acd2
Change label to match new GUI label
brtietz Oct 21, 2024
e82add5
Correct comment on interconnection limits
brtietz Oct 22, 2024
8409974
Merge branch 'develop' of https://github.com/NREL/ssc into sam_181_in…
brtietz Oct 24, 2024
36641dc
Update charging with efficiency numbers so energy more consistently m…
brtietz Oct 25, 2024
710fe31
Move curtail charging closer to clip charging to encourage it. Signif…
brtietz Oct 25, 2024
8be7ef0
Merge branch 'develop' of https://github.com/NREL/ssc into sam_181_in…
brtietz Oct 28, 2024
a9986ea
Upgrade forecasting functions to solve ssc #825
brtietz Oct 29, 2024
f376183
Add cleared capacity into decision making for automated FOM dispatch.…
brtietz Oct 30, 2024
5c32c64
Merge branch 'develop' of https://github.com/NREL/ssc into sam_181_in…
brtietz Oct 30, 2024
5dcd686
Add or statements such that battery always tries to charge from curta…
brtietz Oct 30, 2024
6c2c876
Merge branch 'sam_181_interconnection_limits' into sam_391_ssc_825_me…
brtietz Oct 30, 2024
52e33f5
Merge branch 'develop' of https://github.com/NREL/ssc into sam_391_ss…
brtietz Oct 31, 2024
c34d885
Clean up common.cpp and charging/discharging controls for cleared cap…
brtietz Oct 31, 2024
e468d5a
Test updates and new tests for new merchant plant behavior and effici…
brtietz Oct 31, 2024
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
1 change: 1 addition & 0 deletions shared/lib_battery_dispatch.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class dispatch_t
enum CURRENT_CHOICE { RESTRICT_POWER, RESTRICT_CURRENT, RESTRICT_BOTH };
enum CYCLE_COST {MODEL_CYCLE_COST, INPUT_CYCLE_COST};
enum CONNECTION { DC_CONNECTED, AC_CONNECTED };
enum CAPACITY_FORECAST_TYPE { PRICE_ONLY, CAPACITY_ONLY, PRICE_AND_CAPACITY };

dispatch_t(battery_t * Battery,
double dt,
Expand Down
90 changes: 71 additions & 19 deletions shared/lib_battery_dispatch_automatic_fom.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "lib_utility_rate.h"

#include <numeric>
#include <limits>

dispatch_automatic_front_of_meter_t::dispatch_automatic_front_of_meter_t(
battery_t * Battery,
Expand Down Expand Up @@ -71,7 +72,10 @@ dispatch_automatic_front_of_meter_t::dispatch_automatic_front_of_meter_t(
double etaPVCharge,
double etaGridCharge,
double etaDischarge,
double interconnection_limit) : dispatch_automatic_t(Battery, dt_hour, SOC_min, SOC_max, current_choice, Ic_max, Id_max, Pc_max_kwdc, Pd_max_kwdc, Pc_max_kwac, Pd_max_kwac,
double interconnection_limit,
std::vector<double> cleared_capacities_kw,
dispatch_t::CAPACITY_FORECAST_TYPE cleared_capacity_type,
double cleared_cap_percent) : dispatch_automatic_t(Battery, dt_hour, SOC_min, SOC_max, current_choice, Ic_max, Id_max, Pc_max_kwdc, Pd_max_kwdc, Pc_max_kwac, Pd_max_kwac,
t_min, dispatch_mode, weather_forecast_mode, pv_dispatch, nyears, look_ahead_hours, dispatch_update_frequency_hours,
can_charge, can_clip_charge, can_grid_charge, can_fuelcell_charge, can_curtail_charge,
battReplacementCostPerkWh, battCycleCostChoice, battCycleCost, battOMCost, interconnection_limit)
Expand All @@ -82,6 +86,9 @@ dispatch_automatic_front_of_meter_t::dispatch_automatic_front_of_meter_t(

_inverter_paco = inverter_paco;
_forecast_price_rt_series = forecast_price_series_dollar_per_kwh;
cleared_capacities = cleared_capacities_kw;
capacity_type = cleared_capacity_type;
cleared_capacity_percent = cleared_cap_percent;

// only create utility rate calculator if utility rate is defined
if (utilityRate) {
Expand All @@ -107,6 +114,9 @@ void dispatch_automatic_front_of_meter_t::init_with_pointer(const dispatch_autom
_forecast_hours = tmp->_forecast_hours;
_inverter_paco = tmp->_inverter_paco;
_forecast_price_rt_series = tmp->_forecast_price_rt_series;
cleared_capacities = tmp->cleared_capacities;
capacity_type = tmp->capacity_type;
cleared_capacity_percent = tmp->cleared_capacity_percent;
ppa_prices = tmp->ppa_prices;

discharge_hours = tmp->discharge_hours;
Expand Down Expand Up @@ -204,6 +214,15 @@ void dispatch_automatic_front_of_meter_t::update_dispatch(size_t year, size_t ho
auto charge_ppa_cost = ppa_prices[discharge_hours];
auto discharge_ppa_cost = ppa_prices[ppa_prices.size() - discharge_hours - 1];
double ppa_cost = _forecast_price_rt_series[lifetimeIndex];
double cleared_capacity = std::numeric_limits<double>::max();
double cleared_capacity_energy_required = 0.0;
if (capacity_type != PRICE_ONLY && cleared_capacities.size() > lifetimeIndex) {
// Divide by zero error covered by != PRICE_ONLY above (see common.cpp)
cleared_capacity = cleared_capacities[lifetimeIndex] / cleared_capacity_percent;
if (cleared_capacities.size() > (lifetimeIndex + idx_lookahead)) {
cleared_capacity_energy_required = std::accumulate(cleared_capacities.begin() + lifetimeIndex, cleared_capacities.begin() + lifetimeIndex + idx_lookahead, cleared_capacity_energy_required) * _dt_hour;
}
}

/*! Cost to purchase electricity from the utility */
double usage_cost = ppa_cost;
Expand Down Expand Up @@ -274,8 +293,21 @@ void dispatch_automatic_front_of_meter_t::update_dispatch(size_t year, size_t ho
/*! Energy need to charge the battery (kWh) */
double energyNeededToFillBattery = _Battery->energy_to_fill(m_batteryPower->stateOfChargeMax);

cleared_capacity_energy_required -= _Battery->energy_available(m_batteryPower->stateOfChargeMin);
bool need_to_charge_for_cleared_capacity = cleared_capacity_energy_required > 0.0 || cleared_capacity < 0.0;

double grid_capacity = std::fmin(m_batteryPower->powerInterconnectionLimit, m_batteryPower->powerCurtailmentLimit);

bool discharge_to_meet_cleared_capacity_w_price = false;
if (capacity_type == dispatch_t::PRICE_AND_CAPACITY) {
discharge_to_meet_cleared_capacity_w_price = ((cleared_capacity - m_batteryPower->powerSystem - m_batteryPower->powerPVInverterDraw) > 0);
}

if (capacity_type != dispatch_t::PRICE_ONLY) {
grid_capacity = std::fmin(cleared_capacity, grid_capacity);
}
// Positive: spare power to discharge. Negative: system power will be curtailed. Negative number can be fed directly into powerBattery to support charging
double interconnectionCapacity = std::fmin(m_batteryPower->powerInterconnectionLimit, m_batteryPower->powerCurtailmentLimit) - m_batteryPower->powerSystem;
double interconnectionCapacity = grid_capacity - m_batteryPower->powerSystem - m_batteryPower->powerPVInverterDraw; // Add in PV inverter draw to support meeting cleared capacity for merchant plant

/* Booleans to assist decisions */
bool highDischargeValuePeriod = ppa_cost >= discharge_ppa_cost && ppa_cost >= charge_ppa_cost;
Expand All @@ -299,10 +331,15 @@ void dispatch_automatic_front_of_meter_t::update_dispatch(size_t year, size_t ho
powerBattery = interconnectionCapacity * m_batteryPower->singlePointEfficiencyACToDC;
}

double system_to_grid_reserve = 0.0;
if (capacity_type != PRICE_ONLY) {
system_to_grid_reserve = cleared_capacity > 0 ? cleared_capacity : 0.0;
}

// Increase charge from system (PV) if it is more valuable later than selling now
if (m_batteryPower->canSystemCharge &&
revenueToPVCharge > 0 &&
highChargeValuePeriod &&
((revenueToPVCharge > 0 &&
highChargeValuePeriod) || need_to_charge_for_cleared_capacity) &&
m_batteryPower->powerSystem > 0)
{
// leave EnergyToStoreClipped capacity in battery
Expand All @@ -312,50 +349,65 @@ void dispatch_automatic_front_of_meter_t::update_dispatch(size_t year, size_t ho
{
double energyCanCharge = (energyNeededToFillBattery - energyToStoreClipped);
if (energyCanCharge <= m_batteryPower->powerSystem * _dt_hour)
powerBattery = -std::fmax(energyCanCharge / _dt_hour, m_batteryPower->powerSystemClipped);
powerBattery = -std::fmax(energyCanCharge / _dt_hour - system_to_grid_reserve, m_batteryPower->powerSystemClipped);
else
powerBattery = -std::fmax(m_batteryPower->powerSystem, m_batteryPower->powerSystemClipped);
powerBattery = -std::fmax(m_batteryPower->powerSystem - system_to_grid_reserve, m_batteryPower->powerSystemClipped);

energyNeededToFillBattery = std::fmax(0, energyNeededToFillBattery + (powerBattery * _dt_hour));
}

}
// otherwise, don't reserve capacity for clipping
else {
powerBattery = -m_batteryPower->powerSystem;
powerBattery = -m_batteryPower->powerSystem + system_to_grid_reserve;
}
}

// Also charge from grid if it is valuable to do so, still leaving EnergyToStoreClipped capacity in battery
// TODO: Grid charging needs to check cleared capacity!
if (m_batteryPower->canGridCharge &&
revenueToGridCharge >= revenueToPVChargeMax &&
((revenueToGridCharge >= revenueToPVChargeMax &&
revenueToGridCharge > 0 &&
highChargeValuePeriod &&
highChargeValuePeriod) || need_to_charge_for_cleared_capacity) &&
energyNeededToFillBattery > 0)
{
// leave EnergyToStoreClipped capacity in battery
if (m_batteryPower->canClipCharge)
{
if (energyToStoreClipped < energyNeededToFillBattery)
if (capacity_type == PRICE_ONLY) {
// leave EnergyToStoreClipped capacity in battery
if (m_batteryPower->canClipCharge)
{
double energyCanCharge = (energyNeededToFillBattery - energyToStoreClipped);
powerBattery -= energyCanCharge / _dt_hour;
if (energyToStoreClipped < energyNeededToFillBattery)
{
double energyCanCharge = (energyNeededToFillBattery - energyToStoreClipped);
powerBattery -= energyCanCharge / _dt_hour;
}
}
else
powerBattery = _Battery->calculate_max_charge_kw();
}
else {
if (m_batteryPower->canSystemCharge) {
powerBattery = cleared_capacity - m_batteryPower->powerSystem;
}
else {
powerBattery = cleared_capacity;
}
}
else
powerBattery = _Battery->calculate_max_charge_kw();
}

// Discharge if we are in a high-price period and have battery and inverter capacity
if (highDischargeValuePeriod && revenueToDischarge > 0 && excessAcCapacity && batteryHasDischargeCapacity && interconnectionHasCapacity) {
bool economic_discharge_period = highDischargeValuePeriod && revenueToDischarge > 0 && excessAcCapacity && batteryHasDischargeCapacity && interconnectionHasCapacity;
bool charge_now_for_capacity_later = need_to_charge_for_cleared_capacity && powerBattery < 0.0;
bool cleared_capacity_discharge_period = interconnectionHasCapacity && (capacity_type == dispatch_t::CAPACITY_ONLY || discharge_to_meet_cleared_capacity_w_price);
if ((economic_discharge_period && !charge_now_for_capacity_later) || cleared_capacity_discharge_period ) {
double loss_kw = _Battery->calculate_loss(m_batteryPower->powerBatteryTarget, lifetimeIndex); // Battery is responsible for covering discharge losses
if (m_batteryPower->connectionMode == BatteryPower::DC_CONNECTED) {
powerBattery = _inverter_paco + loss_kw - m_batteryPower->powerSystem;
}
else {
powerBattery = _inverter_paco; // AC connected battery is already maxed out by AC power limit, cannot increase dispatch to ccover losses
}
powerBattery = std::fmin(powerBattery, interconnectionCapacity);

powerBattery = std::fmin(powerBattery, interconnectionCapacity / m_batteryPower->singlePointEfficiencyDCToAC);
}
// save for extraction
m_batteryPower->powerBatteryTarget = powerBattery;
Expand Down
66 changes: 36 additions & 30 deletions shared/lib_battery_dispatch_automatic_fom.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,41 +47,44 @@ class dispatch_automatic_front_of_meter_t : public dispatch_automatic_t
3. Charging from the PV array during times of low PPA sell rates
4. Charging from the PV array during times where the PV power would be clipped due to inverter limits (if DC-connected)
*/
dispatch_automatic_front_of_meter_t(
battery_t * Battery,
double dt,
double SOC_min,
double SOC_max,
int current_choice,
double Ic_max,
double Id_max,
double Pc_max_kwdc,
double Pd_max_kwdc,
double Pc_max_kwac,
double Pd_max_kwac,
double t_min,
int dispatch_mode,
dispatch_automatic_front_of_meter_t(
battery_t* Battery,
double dt,
double SOC_min,
double SOC_max,
int current_choice,
double Ic_max,
double Id_max,
double Pc_max_kwdc,
double Pd_max_kwdc,
double Pc_max_kwac,
double Pd_max_kwac,
double t_min,
int dispatch_mode,
int weather_forecast_mode,
int pv_dispatch,
size_t nyears,
size_t look_ahead_hours,
double dispatch_update_frequency_hours,
bool can_charge,
bool can_clipcharge,
bool can_grid_charge,
bool can_fuelcell_charge,
int pv_dispatch,
size_t nyears,
size_t look_ahead_hours,
double dispatch_update_frequency_hours,
bool can_charge,
bool can_clipcharge,
bool can_grid_charge,
bool can_fuelcell_charge,
bool can_curtail_charge,
double inverter_paco,
double inverter_paco,
std::vector<double> battReplacementCostPerkWh,
int battCycleCostChoice,
int battCycleCostChoice,
std::vector<double> battCycleCost,
std::vector<double> battOMCost, // required for base class
std::vector<double> ppa_price_series_dollar_per_kwh,
UtilityRate * utilityRate,
double etaPVCharge,
double etaGridCharge,
double etaDischarge,
double interconnection_limit
std::vector<double> ppa_price_series_dollar_per_kwh,
UtilityRate* utilityRate,
double etaPVCharge,
double etaGridCharge,
double etaDischarge,
double interconnection_limit,
std::vector<double> cleared_capacities_kw,
dispatch_t::CAPACITY_FORECAST_TYPE cleared_capacity_type,
double cleared_cap_percent
);

~dispatch_automatic_front_of_meter_t();
Expand Down Expand Up @@ -127,6 +130,9 @@ class dispatch_automatic_front_of_meter_t : public dispatch_automatic_t
/*! Market real time and forecast prices */
std::vector<double> _forecast_price_rt_series;
std::vector<double> ppa_prices; // Smaller vector for copying and sorting
std::vector<double> cleared_capacities; // For automated dispatch with merchant plant
dispatch_t::CAPACITY_FORECAST_TYPE capacity_type;
double cleared_capacity_percent; // 0 - 1, 0: all markets are set by percent of generation, 1: all markets are set by cleared capacity

size_t discharge_hours; // Battery size in hours

Expand Down
7 changes: 5 additions & 2 deletions ssc/cmod_battery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "common.h"
#include "core.h"
#include "lib_battery.h"
#include "lib_battery_dispatch.h"
#include "lib_battery_dispatch_automatic_btm.h"
#include "lib_battery_dispatch_automatic_fom.h"
#include "lib_battery_dispatch_pvsmoothing_fom.h"
Expand Down Expand Up @@ -625,6 +624,9 @@ battstor::battstor(var_table& vt, bool setup_model, size_t nrec, double dt_hr, c
forecast_price_signal fps(&vt);
fps.setup(step_per_hour);
batt_vars->forecast_price_series_dollar_per_kwh = fps.forecast_price();
batt_vars->forecast_cleared_capacities_kw = fps.cleared_capacity();
batt_vars->capacity_forecast_type = fps.forecast_type;
batt_vars->cleared_capacity_percent = fps.cleared_capacity_percent;
outMarketPrice = vt.allocate("market_sell_rate_series_yr1", batt_vars->forecast_price_series_dollar_per_kwh.size());
for (i = 0; i < batt_vars->forecast_price_series_dollar_per_kwh.size(); i++) {
outMarketPrice[i] = (ssc_number_t)(batt_vars->forecast_price_series_dollar_per_kwh[i] * 1000.0);
Expand Down Expand Up @@ -1326,7 +1328,8 @@ battstor::battstor(var_table& vt, bool setup_model, size_t nrec, double dt_hr, c
batt_vars->inverter_paco, batt_vars->batt_cost_per_kwh,
batt_vars->batt_cycle_cost_choice, batt_vars->batt_cycle_cost, batt_vars->om_batt_variable_cost_per_kwh,
batt_vars->forecast_price_series_dollar_per_kwh, utilityRate,
eta_pvcharge, eta_gridcharge, eta_discharge, batt_vars->grid_interconnection_limit_kW);
eta_pvcharge, eta_gridcharge, eta_discharge, batt_vars->grid_interconnection_limit_kW,
batt_vars->forecast_cleared_capacities_kw, batt_vars->capacity_forecast_type, batt_vars->cleared_capacity_percent);

if (batt_vars->batt_dispatch == dispatch_t::FOM_CUSTOM_DISPATCH)
{
Expand Down
4 changes: 4 additions & 0 deletions ssc/cmod_battery.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "core.h"
#include "lib_battery.h"
#include "lib_utility_rate.h"
#include "lib_battery_dispatch.h"
#include "cmod_utilityrate5.h"

// forward declarations to speed up build
Expand Down Expand Up @@ -220,6 +221,9 @@ struct batt_variables

/*! PPA price */
std::vector<double> forecast_price_series_dollar_per_kwh;
std::vector<double> forecast_cleared_capacities_kw;
dispatch_t::CAPACITY_FORECAST_TYPE capacity_forecast_type;
double cleared_capacity_percent;

/*! Energy rates */
bool ec_rate_defined, ec_use_realtime;
Expand Down
Loading