diff --git a/ssc/cmod_merchantplant.cpp b/ssc/cmod_merchantplant.cpp index 53c716225..434e96306 100644 --- a/ssc/cmod_merchantplant.cpp +++ b/ssc/cmod_merchantplant.cpp @@ -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 }; @@ -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 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 mp_energy_market_purchases(8760*nyears, 0.0); + std::vector mp_ancillary_services1_purchases(8760*nyears, 0.0); + std::vector mp_ancillary_services2_purchases(8760*nyears, 0.0); + std::vector mp_ancillary_services3_purchases(8760*nyears, 0.0); + std::vector 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) @@ -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]); } } } @@ -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]); } } } diff --git a/ssc/cmod_merchantplant_eqns.cpp b/ssc/cmod_merchantplant_eqns.cpp index fa838e726..d69e4b02a 100644 --- a/ssc/cmod_merchantplant_eqns.cpp +++ b/ssc/cmod_merchantplant_eqns.cpp @@ -156,6 +156,11 @@ bool mp_ancillary_services(ssc_data_t data) std::vector ancillary_services2_revenue(nsteps, 0.0); std::vector ancillary_services3_revenue(nsteps, 0.0); std::vector ancillary_services4_revenue(nsteps, 0.0); + std::vector energy_market_purchases(nsteps, 0.0); + std::vector ancillary_services1_purchases(nsteps, 0.0); + std::vector ancillary_services2_purchases(nsteps, 0.0); + std::vector ancillary_services3_purchases(nsteps, 0.0); + std::vector 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."; @@ -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 + } } } @@ -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; + } + } } @@ -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) { diff --git a/ssc/common.cpp b/ssc/common.cpp index 21184508e..3b0ac8692 100644 --- a/ssc/common.cpp +++ b/ssc/common.cpp @@ -779,7 +779,7 @@ 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()); @@ -787,7 +787,7 @@ bool forecast_price_signal::setup(size_t step_per_hour) 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()); @@ -795,7 +795,7 @@ bool forecast_price_signal::setup(size_t step_per_hour) 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()); @@ -803,7 +803,7 @@ bool forecast_price_signal::setup(size_t step_per_hour) 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()); @@ -811,7 +811,7 @@ bool forecast_price_signal::setup(size_t step_per_hour) 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()); } diff --git a/test/ssc_test/save_as_JSON_test.cpp b/test/ssc_test/save_as_JSON_test.cpp index 2e012df05..676fe0e0c 100644 --- a/test/ssc_test/save_as_JSON_test.cpp +++ b/test/ssc_test/save_as_JSON_test.cpp @@ -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); @@ -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);