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

Ssc 599 negative cleared capacity #826

Merged
merged 7 commits into from
May 31, 2022
29 changes: 24 additions & 5 deletions ssc/cmod_merchantplant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,12 @@ static var_info _cm_vtab_merchantplant[] = {
{ SSC_OUTPUT, SSC_ARRAY, "mp_ancillary_services4_price", "Ancillary services 4 generated price", "$/MWh", "", "", "*", "", "GROUP=LIFETIME_MP" },
// sum of all cleared capacities
{ SSC_OUTPUT, SSC_ARRAY, "mp_total_cleared_capacity", "Total cleared capacity", "MW", "", "", "*", "", "GROUP=LIFETIME_MP" },
// calculated revenue
{ SSC_OUTPUT, SSC_ARRAY, "mp_energy_market_consumed_cost", "Energy market electricity purchases", "$", "", "", "*", "", "GROUP=LIFETIME_MP" },
{ SSC_OUTPUT, SSC_ARRAY, "mp_ancillary_services1_consumed_cost", "Ancillary services 1 electricity purchases", "$", "", "", "*", "", "GROUP=LIFETIME_MP" },
{ SSC_OUTPUT, SSC_ARRAY, "mp_ancillary_services2_consumed_cost", "Ancillary services 2 electricity purchases", "$", "", "", "*", "", "GROUP=LIFETIME_MP" },
{ SSC_OUTPUT, SSC_ARRAY, "mp_ancillary_services3_consumed_cost", "Ancillary services 3 electricity purchases", "$", "", "", "*", "", "GROUP=LIFETIME_MP" },
{ SSC_OUTPUT, SSC_ARRAY, "mp_ancillary_services4_consumed_cost", "Ancillary services 4 electricity purchases", "$", "", "", "*", "", "GROUP=LIFETIME_MP" },

var_info_invalid };

Expand Down Expand Up @@ -1372,9 +1378,22 @@ class cm_merchantplant : public compute_module
cf.at(CF_om_opt_fuel_2_expense,i) *= om_opt_fuel_2_usage;
}

std::vector<double> mp_energy_market_price(8760*nyears, 0.0);
if (lookup("mp_energy_market_price"))
mp_energy_market_price = lookup("mp_energy_market_price")->arr_vector();
std::vector<double> mp_energy_market_purchases(8760*nyears, 0.0);
std::vector<double> mp_ancillary_services1_purchases(8760*nyears, 0.0);
std::vector<double> mp_ancillary_services2_purchases(8760*nyears, 0.0);
std::vector<double> mp_ancillary_services3_purchases(8760*nyears, 0.0);
std::vector<double> mp_ancillary_services4_purchases(8760*nyears, 0.0);
if (lookup("mp_energy_market_consumed_cost"))
mp_energy_market_purchases = lookup("mp_energy_market_consumed_cost")->arr_vector();
if (lookup("mp_ancillary_services1_consumed_cost"))
mp_ancillary_services1_purchases = lookup("mp_ancillary_services1_consumed_cost")->arr_vector();
if (lookup("mp_ancillary_services2_consumed_cost"))
mp_ancillary_services2_purchases = lookup("mp_ancillary_services2_consumed_cost")->arr_vector();
if (lookup("mp_ancillary_services3_consumed_cost"))
mp_ancillary_services3_purchases = lookup("mp_ancillary_services3_consumed_cost")->arr_vector();
if (lookup("mp_ancillary_services4_consumed_cost"))
mp_ancillary_services4_purchases = lookup("mp_ancillary_services4_consumed_cost")->arr_vector();

bool ppa_purchases = !(is_assigned("en_electricity_rates") && as_number("en_electricity_rates") == 1);

if (as_integer("system_use_lifetime_output") == 1)
Expand All @@ -1388,7 +1407,7 @@ class cm_merchantplant : public compute_module

for (size_t h = 0; h < 8760; h++) {
if (ppa_purchases) {
cf.at(CF_utility_bill, i) += -hourly_energy_calcs.hourly_purchases()[(i - 1) * 8760 + h] * cf.at(CF_degradation, i) * mp_energy_market_price[(i -1) * 8760 + h];
cf.at(CF_utility_bill, i) -= (mp_energy_market_purchases[(i - 1) * 8760 + h] + mp_ancillary_services1_purchases[(i - 1) * 8760 + h] + mp_ancillary_services2_purchases[(i - 1) * 8760 + h] + mp_ancillary_services3_purchases[(i - 1) * 8760 + h] + mp_ancillary_services4_purchases[(i - 1) * 8760 + h]);
}
}
}
Expand All @@ -1399,7 +1418,7 @@ class cm_merchantplant : public compute_module

for (size_t h = 0; h < 8760; h++) {
if (ppa_purchases) {
cf.at(CF_utility_bill, i) += -hourly_energy_calcs.hourly_purchases()[h] * cf.at(CF_degradation, i) * mp_energy_market_price[h];
cf.at(CF_utility_bill, i) -= cf.at(CF_degradation, i) * (mp_energy_market_purchases[h] + mp_ancillary_services1_purchases[h] + mp_ancillary_services2_purchases[h] + mp_ancillary_services3_purchases[h] + mp_ancillary_services4_purchases[h]);
}
}
}
Expand Down
124 changes: 72 additions & 52 deletions ssc/cmod_merchantplant_eqns.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ bool mp_ancillary_services(ssc_data_t data)
std::vector<ssc_number_t> ancillary_services2_revenue(nsteps, 0.0);
std::vector<ssc_number_t> ancillary_services3_revenue(nsteps, 0.0);
std::vector<ssc_number_t> ancillary_services4_revenue(nsteps, 0.0);
std::vector<ssc_number_t> energy_market_purchases(nsteps, 0.0);
std::vector<ssc_number_t> ancillary_services1_purchases(nsteps, 0.0);
std::vector<ssc_number_t> ancillary_services2_purchases(nsteps, 0.0);
std::vector<ssc_number_t> ancillary_services3_purchases(nsteps, 0.0);
std::vector<ssc_number_t> ancillary_services4_purchases(nsteps, 0.0);

if (ancillary_services_success) {
warning = "No source of revenue enabled. The Merchant Plant financial model requires at least one source of revenue. To fix the problem, enable energy market revenue and/or one or more ancillary services.";
Expand Down Expand Up @@ -547,37 +552,17 @@ bool mp_ancillary_services(ssc_data_t data)
{
for (size_t i = 0; (i < cleared_capacity_sum.size()) && (i < system_generation.size()); i++)
{
/*
if (energy_market_capacity[i] < 0)
{
error = util::format("energy market cleared capacity %g is less than zero at timestep %d", energy_market_capacity[i], int(i));
break;
}
else if (ancillary_services1_capacity[i] < 0)
{
error = util::format("ancillary services 1 market cleared capacity %g is less than zero at timestep %d", ancillary_services1_capacity[i], int(i));
break;
}
else if (ancillary_services2_capacity[i] < 0)
{
error = util::format("ancillary services 2 market cleared capacity %g is less than zero at timestep %d", ancillary_services2_capacity[i], int(i));
break;
}
else if (ancillary_services3_capacity[i] < 0)
{
error = util::format("ancillary services 3 market cleared capacity %g is less than zero at timestep %d", ancillary_services3_capacity[i], int(i));
break;
}
else if (ancillary_services4_capacity[i] < 0)
{
error = util::format("ancillary services 4 market cleared capacity %g is less than zero at timestep %d", ancillary_services4_capacity[i], int(i));
break;
}
else */ if ((cleared_capacity_sum[i] > 0) && ((cleared_capacity_sum[i] - system_generation[i]) > 1e-5 * abs(system_generation[i]) ))
if ((cleared_capacity_sum[i] > 0) && ((cleared_capacity_sum[i] - system_generation[i]) > 1e-5 * abs(system_generation[i]) ))
{
error = util::format("sum of cleared capacity %g MW does not match system capacity %g MW at timestep %d", cleared_capacity_sum[i], system_generation[i], int(i));
break;
}

if ((cleared_capacity_sum[i] < 0) && ((cleared_capacity_sum[i] - system_generation[i]) > 1e-5 * abs(system_generation[i])))
{
warning = util::format("sum of cleared capacity %g MW does not match system capacity %g MW at timestep %d", cleared_capacity_sum[i], system_generation[i], int(i));
// Don't break in case there's an error at a later step. Note that the current behavior reports the last step at which there was a cleared capacity issue
}
}
}

Expand All @@ -596,31 +581,62 @@ bool mp_ancillary_services(ssc_data_t data)
vt->assign("mp_ancillary_services4_price", var_data(ancillary_services4_revenue.data(), ancillary_services4_revenue.size()));
// total cleared capacity
vt->assign("mp_total_cleared_capacity", var_data(cleared_capacity_sum.data(), cleared_capacity_sum.size()));
for (size_t i = 0; (i < system_generation.size()) && (i < energy_market_capacity.size()) && (i < energy_market_revenue.size()); i++)

ssc_number_t revenue = 0.0;
for (size_t i = 0; (i < system_generation.size()) && (i < energy_market_capacity.size()) && (i < energy_market_revenue.size()); i++)
{
// if (fabs(cleared_capacity_sum[i]) < 1e-5) // override, compensate generation at first enabled market if greater than zero.
{
if (en_mp_energy_market)
energy_market_revenue[i] *= energy_market_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
else
energy_market_revenue[i] = 0.0;
if (en_mp_ancserv1)
ancillary_services1_revenue[i] *= ancillary_services1_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
else
ancillary_services1_revenue[i] = 0.0;
if (en_mp_ancserv2)
ancillary_services2_revenue[i] *= ancillary_services2_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
else
ancillary_services2_revenue[i] = 0.0;
if (en_mp_ancserv3)
ancillary_services3_revenue[i] *= ancillary_services3_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
else
ancillary_services3_revenue[i] = 0.0;
if (en_mp_ancserv4)
ancillary_services4_revenue[i] *= ancillary_services4_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
else
ancillary_services4_revenue[i] = 0.0;
}

// Only count positive revenue here. Negative numbers are an operating expense for LCOE
if (en_mp_energy_market) {
revenue = energy_market_revenue[i] * energy_market_capacity[i] / steps_per_hour;// [MW] * [$/MWh] / fraction per hour [1/h]
energy_market_revenue[i] = revenue > 0 ? revenue : 0.0;
energy_market_purchases[i] = revenue < 0 ? revenue : 0.0;
}
else {
energy_market_revenue[i] = 0.0;
energy_market_purchases[i] = 0.0;
}

if (en_mp_ancserv1) {
revenue = ancillary_services1_revenue[i] * ancillary_services1_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
ancillary_services1_revenue[i] = revenue > 0 ? revenue : 0.0;
ancillary_services1_purchases[i] = revenue < 0 ? revenue : 0.0;
}
else {
ancillary_services1_revenue[i] = 0.0;
ancillary_services1_purchases[i] = 0.0;
}

if (en_mp_ancserv2) {
revenue = ancillary_services2_revenue[i] * ancillary_services2_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
ancillary_services2_revenue[i] = revenue > 0 ? revenue : 0.0;
ancillary_services2_purchases[i] = revenue < 0 ? revenue : 0.0;
}
else {
ancillary_services2_revenue[i] = 0.0;
ancillary_services2_purchases[i] = 0.0;
}

if (en_mp_ancserv3) {
revenue = ancillary_services3_revenue[i] * ancillary_services3_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
ancillary_services3_revenue[i] = revenue > 0 ? revenue : 0.0;
ancillary_services3_purchases[i] = revenue < 0 ? revenue : 0.0;
}
else {
ancillary_services3_revenue[i] = 0.0;
ancillary_services3_purchases[i] = 0.0;
}

if (en_mp_ancserv4) {
revenue = ancillary_services4_revenue[i] * ancillary_services4_capacity[i] / steps_per_hour; // [MW] * [$/MWh] / fraction per hour [1/h]
ancillary_services4_revenue[i] = revenue > 0 ? revenue : 0.0;
ancillary_services4_purchases[i] = revenue < 0 ? revenue : 0.0;
}
else {
ancillary_services4_revenue[i] = 0.0;
ancillary_services4_purchases[i] = 0.0;
}

}
}

Expand All @@ -637,7 +653,11 @@ bool mp_ancillary_services(ssc_data_t data)
vt->assign("mp_ancillary_services3_generated_revenue", var_data(ancillary_services3_revenue.data(), ancillary_services3_revenue.size()));
vt->assign("mp_ancillary_services4_generated_revenue", var_data(ancillary_services4_revenue.data(), ancillary_services4_revenue.size()));


vt->assign("mp_energy_market_consumed_cost", var_data(energy_market_purchases.data(), energy_market_purchases.size()));
vt->assign("mp_ancillary_services1_consumed_cost", var_data(ancillary_services1_purchases.data(), ancillary_services1_purchases.size()));
vt->assign("mp_ancillary_services2_consumed_cost", var_data(ancillary_services2_purchases.data(), ancillary_services2_purchases.size()));
vt->assign("mp_ancillary_services3_consumed_cost", var_data(ancillary_services3_purchases.data(), ancillary_services3_purchases.size()));
vt->assign("mp_ancillary_services4_consumed_cost", var_data(ancillary_services4_purchases.data(), ancillary_services4_purchases.size()));
}
catch (std::exception& e)
{
Expand Down
10 changes: 5 additions & 5 deletions ssc/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -779,39 +779,39 @@ bool forecast_price_signal::setup(size_t step_per_hour)
as_revenue.clear();
as_revenue.reserve(n_marketrevenue_per_year);
for (size_t j = y * n_marketrevenue_per_year; j < (y + 1) * n_marketrevenue_per_year; j++)
as_revenue.push_back(mp_energy_market_revenue_mat.at(j, 1) / 1000.0);
as_revenue.push_back(mp_energy_market_revenue_mat.at(j, 1 - mp_enable_market_percent_gen) / 1000.0);
as_revenue_extrapolated = extrapolate_timeseries(as_revenue, step_per_hour);
std::transform(m_forecast_price.begin() + forecast_start, m_forecast_price.begin() + forecast_end, as_revenue_extrapolated.begin(), m_forecast_price.begin() + forecast_start, std::plus<double>());

size_t n_ancserv_1_revenue_per_year = mp_ancserv_1_revenue_mat.nrows() / (size_t)nyears;
as_revenue.clear();
as_revenue.reserve(n_ancserv_1_revenue_per_year);
for (size_t j = y * n_ancserv_1_revenue_per_year; j < (y + 1) * n_ancserv_1_revenue_per_year; j++)
as_revenue.push_back(mp_ancserv_1_revenue_mat.at(j, 1) / 1000.0);
as_revenue.push_back(mp_ancserv_1_revenue_mat.at(j, 1 - mp_enable_ancserv1_percent_gen) / 1000.0);
as_revenue_extrapolated = extrapolate_timeseries(as_revenue, step_per_hour);
std::transform(m_forecast_price.begin() + forecast_start, m_forecast_price.begin() + forecast_end, as_revenue_extrapolated.begin(), m_forecast_price.begin() + forecast_start, std::plus<double>());

size_t n_ancserv_2_revenue_per_year = mp_ancserv_2_revenue_mat.nrows() / (size_t)nyears;
as_revenue.clear();
as_revenue.reserve(n_ancserv_2_revenue_per_year);
for (size_t j = y * n_ancserv_2_revenue_per_year; j < (y + 1) * n_ancserv_2_revenue_per_year; j++)
as_revenue.push_back(mp_ancserv_2_revenue_mat.at(j, 1) / 1000.0);
as_revenue.push_back(mp_ancserv_2_revenue_mat.at(j, 1 - mp_enable_ancserv2_percent_gen) / 1000.0);
as_revenue_extrapolated = extrapolate_timeseries(as_revenue, step_per_hour);
std::transform(m_forecast_price.begin() + forecast_start, m_forecast_price.begin() + forecast_end, as_revenue_extrapolated.begin(), m_forecast_price.begin() + forecast_start, std::plus<double>());

size_t n_ancserv_3_revenue_per_year = mp_ancserv_3_revenue_mat.nrows() / (size_t)nyears;
as_revenue.clear();
as_revenue.reserve(n_ancserv_3_revenue_per_year);
for (size_t j = y * n_ancserv_3_revenue_per_year; j < (y + 1) * n_ancserv_3_revenue_per_year; j++)
as_revenue.push_back(mp_ancserv_3_revenue_mat.at(j, 1) / 1000.0);
as_revenue.push_back(mp_ancserv_3_revenue_mat.at(j, 1 - mp_enable_ancserv3_percent_gen) / 1000.0);
as_revenue_extrapolated = extrapolate_timeseries(as_revenue, step_per_hour);
std::transform(m_forecast_price.begin() + forecast_start, m_forecast_price.begin() + forecast_end, as_revenue_extrapolated.begin(), m_forecast_price.begin() + forecast_start, std::plus<double>());

size_t n_ancserv_4_revenue_per_year = mp_ancserv_4_revenue_mat.nrows() / (size_t)nyears;
as_revenue.clear();
as_revenue.reserve(n_ancserv_4_revenue_per_year);
for (size_t j = y * n_ancserv_4_revenue_per_year; j < (y + 1) * n_ancserv_4_revenue_per_year; j++)
as_revenue.push_back(mp_ancserv_4_revenue_mat.at(j, 1) / 1000.0);
as_revenue.push_back(mp_ancserv_4_revenue_mat.at(j, 1 - mp_enable_ancserv4_percent_gen) / 1000.0);
as_revenue_extrapolated = extrapolate_timeseries(as_revenue, step_per_hour);
std::transform(m_forecast_price.begin() + forecast_start, m_forecast_price.begin() + forecast_end, as_revenue_extrapolated.begin(), m_forecast_price.begin() + forecast_start, std::plus<double>());
}
Expand Down
4 changes: 2 additions & 2 deletions test/ssc_test/save_as_JSON_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ TEST(save_as_JSON_test_run, pv_batt_mechant_plant_rapidjson) {
EXPECT_TRUE(success);
ssc_number_t npv;
ssc_data_get_number(data, "project_return_aftertax_npv", &npv);
EXPECT_NEAR(npv, -82495656, fabs(-82401180) / 1e6);
EXPECT_NEAR(npv, -60972106, fabs(-60972106) / 1e6);

ssc_module_free(mod_pv);
ssc_module_free(mod_grid);
Expand Down Expand Up @@ -216,7 +216,7 @@ TEST(save_as_JSON_test_run, pt_mechant_plant_rapidjson) {
EXPECT_TRUE(success);
ssc_number_t npv;
ssc_data_get_number(data, "project_return_aftertax_npv", &npv);
EXPECT_NEAR(npv, -1648816494, fabs(-1648816494) / 1e7);
EXPECT_NEAR(npv, -570719411, fabs(-570719411) / 1e7);

ssc_module_free(mod_pv);
ssc_module_free(mod_grid);
Expand Down