From bfca1ea781ace1cea74ab1692f439e94a7090d26 Mon Sep 17 00:00:00 2001 From: Brian Mirletz Date: Mon, 9 Dec 2024 09:57:13 -0700 Subject: [PATCH] Ssc 1256 better hybrid batteries (#1258) * Update pGen with battery outputs * Update hybrids to better include annual energy in metrics. Add exceptions for length issues. Add energy metrics to tests * Correct PV scaling units for hybrid O&M * Update hybrid tests for changing pv capacity O&M basis --------- Co-authored-by: Matt Prilliman <54449384+mjprilliman@users.noreply.github.com> --- ssc/cmod_hybrid.cpp | 63 +++++++++++++++------------- ssc/cmod_pvsamv1.cpp | 3 ++ ssc/cmod_pvwattsv8.cpp | 2 + test/ssc_test/cmod_hybrid_test.cpp | 67 +++++++++++++++++++++++------- 4 files changed, 93 insertions(+), 42 deletions(-) diff --git a/ssc/cmod_hybrid.cpp b/ssc/cmod_hybrid.cpp index a430bc3bb..a3f06b16c 100644 --- a/ssc/cmod_hybrid.cpp +++ b/ssc/cmod_hybrid.cpp @@ -155,10 +155,6 @@ class cm_hybrid : public compute_module ssc_module_exec_with_error(module, input, compute_module); - ssc_number_t system_capacity = compute_module_inputs->table.lookup("system_capacity")->num; - hybridSystemCapacity += system_capacity; - hybridTotalInstalledCost += compute_module_inputs->table.lookup("total_installed_cost")->num; - ssc_data_t compute_module_outputs = ssc_data_create(); int pidx = 0; @@ -174,6 +170,13 @@ class cm_hybrid : public compute_module if (compute_module_inputs->table.lookup("system_use_lifetime_output")) system_use_lifetime_output = compute_module_inputs->table.lookup("system_use_lifetime_output")->num; + ssc_number_t system_capacity = compute_module_inputs->table.lookup("system_capacity")->num; + if ((compute_module == "pvsamv1") || (compute_module == "pvwattsv8")) { + ssc_data_get_number(compute_module_outputs, "system_capacity_ac", &system_capacity); + } + hybridSystemCapacity += system_capacity; + hybridTotalInstalledCost += compute_module_inputs->table.lookup("total_installed_cost")->num; + // get minimum timestep from gen vector ssc_number_t* curGen = ssc_data_get_array(compute_module_outputs, "gen", &len); currentTimeStepsPerHour = len / 8760; @@ -287,26 +290,6 @@ class cm_hybrid : public compute_module } } - // monthly energy generated - size_t step_per_hour = maximumTimeStepsPerHour; - ssc_number_t* pGenMonthly = ((var_table*)outputs)->allocate("monthly_energy", 12); - size_t c = 0; - for (int m = 0; m < 12; m++) // each month - { - pGenMonthly[m] = 0; - for (size_t d = 0; d < util::nday[m]; d++) // for each day in each month - for (int h = 0; h < 24; h++) // for each hour in each day - for (size_t j = 0; j < step_per_hour; j++) - pGenMonthly[m] += pGen[c++]; - } - - ssc_number_t pGenAnnual = 0; - for (size_t i = 0; i < genLength; i++) - pGenAnnual += pGen[i]; - ((var_table*)outputs)->assign("annual_energy", var_data(pGenAnnual)); - - - if (fuelcells.size() > 0) { // run single fuel cell if present percent = 100.0f * ((float)(generators.size() + fuelcells.size()) / (float)(generators.size() + fuelcells.size() + batteries.size() + financials.size())); @@ -564,8 +547,35 @@ class cm_hybrid : public compute_module if (batteries.size() > 0) { use_batt_output = true; pBattGen = ((var_table*)outputs)->lookup(batteries[0])->table.as_array("gen", &battGenLen); + if (battGenLen == genLength) { + for (size_t g = 0; g < genLength; g++) { + // Batt's gen is an inout that includes other system components + pGen[g] = pBattGen[g]; + } + } + else { + throw exec_error("hybrid", util::format("Battery gen length incorrect, battery timeseries contains %d entries, generators contain %d", battGenLen, genLength)); + } + } + + // monthly energy generated + size_t step_per_hour = maximumTimeStepsPerHour; + ssc_number_t* pGenMonthly = ((var_table*)outputs)->allocate("monthly_energy", 12); + size_t c = 0; + for (int m = 0; m < 12; m++) // each month + { + pGenMonthly[m] = 0; + for (size_t d = 0; d < util::nday[m]; d++) // for each day in each month + for (int h = 0; h < 24; h++) // for each hour in each day + for (size_t j = 0; j < step_per_hour; j++) + pGenMonthly[m] += pGen[c++]; } + ssc_number_t pGenAnnual = 0; + for (size_t i = 0; i < genLength; i++) + pGenAnnual += pGen[i]; + ((var_table*)outputs)->assign("annual_energy", var_data(pGenAnnual)); + ssc_number_t* pHybridOMSum = ((var_table*)outputs)->allocate("cf_hybrid_om_sum", analysisPeriod + 1); // add to top level "output" - assumes analysis period the same for all generators for (int i = 0; i <= analysisPeriod; i++) @@ -636,10 +646,7 @@ class cm_hybrid : public compute_module var_data* compute_module_inputs = input_table->table.lookup(hybridVarTable); var_table& input = compute_module_inputs->table; - // if (use_batt_output) - // ssc_data_set_array(static_cast(&input), "gen", pBattGen, (int)battGenLen); - // else - ssc_data_set_array(static_cast(&input), "gen", pGen, (int)genLength); + ssc_data_set_array(static_cast(&input), "gen", pGen, (int)genLength); if (batteries.size() > 0) ssc_data_set_number(static_cast(&input), "is_hybrid", 1); // for updating battery outputs to annual length in update_battery_outputs in common_financial.cpp diff --git a/ssc/cmod_pvsamv1.cpp b/ssc/cmod_pvsamv1.cpp index 40fc2a3f3..bcc3f3a92 100644 --- a/ssc/cmod_pvsamv1.cpp +++ b/ssc/cmod_pvsamv1.cpp @@ -1031,6 +1031,8 @@ static var_info _cm_vtab_pvsamv1[] = { //miscellaneous outputs { SSC_OUTPUT, SSC_NUMBER, "ts_shift_hours", "Sun position time offset", "hours", "", "Miscellaneous", "", "", "" }, { SSC_OUTPUT, SSC_NUMBER, "nameplate_dc_rating", "System nameplate DC rating", "kW", "", "Miscellaneous", "*", "", "" }, + { SSC_OUTPUT, SSC_NUMBER, "system_capacity_ac", "System nameplate AC rating", "kWac", "", "Miscellaneous", "", "", "" }, + // test outputs @@ -3594,6 +3596,7 @@ void cm_pvsamv1::exec() kWhACperkWAC = annual_energy / nameplate_ac_kW; } assign("capacity_factor_ac", var_data((ssc_number_t)(kWhACperkWAC / 87.6))); + assign("system_capacity_ac", var_data((ssc_number_t)nameplate_ac_kW)); if (is_assigned("load")) { diff --git a/ssc/cmod_pvwattsv8.cpp b/ssc/cmod_pvwattsv8.cpp index 7346e0abf..0c55fdb27 100644 --- a/ssc/cmod_pvwattsv8.cpp +++ b/ssc/cmod_pvwattsv8.cpp @@ -229,6 +229,7 @@ static var_info _cm_vtab_pvwattsv8[] = { { SSC_OUTPUT, SSC_NUMBER, "ts_shift_hours", "Time offset for interpreting time series outputs", "hours","", "Miscellaneous", "*", "", "" }, { SSC_OUTPUT, SSC_NUMBER, "percent_complete", "Estimated percent of total completed simulation", "%", "", "Miscellaneous", "", "", "" }, + { SSC_OUTPUT, SSC_NUMBER, "system_capacity_ac", "System nameplate AC rating", "kWac", "", "Miscellaneous", "", "", "" }, var_info_invalid }; @@ -1442,6 +1443,7 @@ class cm_pvwattsv8 : public compute_module // for battery model, specify a number of inverters assign("inverter_efficiency", var_data((ssc_number_t)(as_double("inv_eff")))); + assign("system_capacity_ac", var_data((ssc_number_t)pv.ac_nameplate / 1000.0)); if (en_snowloss && snowmodel.badValues > 0) log(util::format("The snow model has detected %d bad snow depth values (less than 0 or greater than 610 cm). These values have been set to zero.", snowmodel.badValues), SSC_WARNING); diff --git a/test/ssc_test/cmod_hybrid_test.cpp b/test/ssc_test/cmod_hybrid_test.cpp index 530d76b53..eef9ba5a7 100644 --- a/test/ssc_test/cmod_hybrid_test.cpp +++ b/test/ssc_test/cmod_hybrid_test.cpp @@ -67,7 +67,9 @@ TEST_F(CmodHybridTest, PVWattsv8WindBatterySingleOwner) { { int len; - ssc_number_t pvannualenergy, windannualenergy, battannualenergy, npv; + ssc_number_t pvannualenergy, windannualenergy, battannualenergy, npv, total_energy; + ssc_number_t* battchargeenergy; + ssc_number_t* battdischargeenergy; auto outputs = ssc_data_get_table(dat, "output"); auto inputs = ssc_data_get_table(dat, "input"); @@ -84,21 +86,27 @@ TEST_F(CmodHybridTest, PVWattsv8WindBatterySingleOwner) { auto batt_outputs = ssc_data_get_table(outputs, "battery"); auto batt_inputs = ssc_data_get_table(inputs, "battery"); ssc_data_get_number(batt_outputs, "annual_energy", &battannualenergy); + battchargeenergy = ssc_data_get_array(batt_outputs, "batt_annual_charge_energy", &len); + battdischargeenergy = ssc_data_get_array(batt_outputs, "batt_annual_discharge_energy", &len); EXPECT_NEAR(battannualenergy, 570565000, 570565000 * 0.01); + EXPECT_NEAR(battchargeenergy[1], 81116970, 81116970 * 0.001); + EXPECT_NEAR(battdischargeenergy[1], 72917429, 72917429 * 0.001); auto hybrid_outputs = ssc_data_get_table(outputs, "Hybrid"); - - ssc_number_t value; + ssc_data_get_number(hybrid_outputs, "annual_energy", &total_energy); auto ebitda = ssc_data_get_array(hybrid_outputs, "cf_ebitda", &len); auto revenue = ssc_data_get_array(hybrid_outputs, "cf_total_revenue", &len); auto om_expenses = ssc_data_get_array(hybrid_outputs, "cf_operating_expenses", &len); ssc_data_get_number(hybrid_outputs, "project_return_aftertax_npv", &npv); - EXPECT_NEAR(om_expenses[1], 10772001, 1); - EXPECT_NEAR(revenue[1], 31651347, 1); - EXPECT_NEAR(ebitda[1], 20879346, 1); - EXPECT_NEAR(npv, -246312045, 246312045 * 0.001); + EXPECT_NEAR(om_expenses[1], 10425847, 1); + EXPECT_NEAR(revenue[1], 33062516, 1); + EXPECT_NEAR(ebitda[1], 22636669, 1); + EXPECT_NEAR(npv, -227222606, 227222606 * 0.001); + + EXPECT_NEAR(total_energy, battannualenergy, total_energy * 0.001); + EXPECT_NEAR(total_energy, pvannualenergy + windannualenergy - battchargeenergy[1] + battdischargeenergy[1], total_energy * 0.001); } ssc_data_free(dat); dat = nullptr; @@ -134,7 +142,10 @@ TEST_F(CmodHybridTest, PVWattsv8WindBatteryHostDeveloper) { EXPECT_FALSE(errors); if (!errors) { - ssc_number_t pvannualenergy, windannualenergy, npv; + int len; + ssc_number_t pvannualenergy, windannualenergy, battannualenergy, npv, total_energy; + ssc_number_t* battchargeenergy; + ssc_number_t* battdischargeenergy; auto outputs = ssc_data_get_table(dat, "output"); auto pv_outputs = ssc_data_get_table(outputs, "pvwattsv8"); @@ -145,9 +156,23 @@ TEST_F(CmodHybridTest, PVWattsv8WindBatteryHostDeveloper) { ssc_data_get_number(wind_outputs, "annual_energy", &windannualenergy); EXPECT_NEAR(windannualenergy, 187767, 187767 * 0.01); + //, + auto battery_outputs = ssc_data_get_table(outputs, "battery"); + ssc_data_get_number(battery_outputs, "annual_energy", &battannualenergy); + battchargeenergy = ssc_data_get_array(battery_outputs, "batt_annual_charge_energy", &len); + battdischargeenergy = ssc_data_get_array(battery_outputs, "batt_annual_discharge_energy", &len); + EXPECT_NEAR(battannualenergy, 1118877, 1118877 * 0.01); + EXPECT_NEAR(battchargeenergy[1], 83565, 83565 * 0.001); + EXPECT_NEAR(battdischargeenergy[1], 76334, 76334 * 0.001); + auto hybrid_outputs = ssc_data_get_table(outputs, "Hybrid"); ssc_data_get_number(hybrid_outputs, "project_return_aftertax_npv", &npv); - EXPECT_NEAR(npv, -182666, 182665 * 0.001); + EXPECT_NEAR(npv, -168769, 168769 * 0.001); + + ssc_data_get_number(hybrid_outputs, "annual_energy", &total_energy); + + EXPECT_NEAR(total_energy, battannualenergy, total_energy * 0.001); + EXPECT_NEAR(total_energy, pvannualenergy + windannualenergy - battchargeenergy[1] + battdischargeenergy[1], total_energy * 0.001); } ssc_data_free(dat); dat = nullptr; @@ -179,7 +204,10 @@ TEST_F(CmodHybridTest, CustomGenerationPVWattsWindFuelCellBatteryHybrid_SingleOw EXPECT_FALSE(errors); if (!errors) { - ssc_number_t genericannualenergy, pvannualenergy, windannualenergy, battannualenergy, npv; + ssc_number_t genericannualenergy, pvannualenergy, windannualenergy, battannualenergy, fuelcellannualenergy, npv, total_energy; + ssc_number_t* battchargeenergy; + ssc_number_t* battdischargeenergy; + int len; auto outputs = ssc_data_get_table(dat, "output"); @@ -195,13 +223,19 @@ TEST_F(CmodHybridTest, CustomGenerationPVWattsWindFuelCellBatteryHybrid_SingleOw ssc_data_get_number(wind_outputs, "annual_energy", &windannualenergy); auto wind_om_expenses = ssc_data_get_array(wind_outputs, "cf_operating_expenses", &len); + auto fuelcell_outputs = ssc_data_get_table(outputs, "fuelcell"); + ssc_data_get_number(fuelcell_outputs, "annual_energy_discharged", &fuelcellannualenergy); + auto batt_outputs = ssc_data_get_table(outputs, "battery"); ssc_data_get_number(batt_outputs, "annual_energy", &battannualenergy); auto batt_om_expenses = ssc_data_get_array(batt_outputs, "cf_operating_expenses", &len); + battchargeenergy = ssc_data_get_array(batt_outputs, "batt_annual_charge_energy", &len); + battdischargeenergy = ssc_data_get_array(batt_outputs, "batt_annual_discharge_energy", &len); auto hybrid_outputs = ssc_data_get_table(outputs, "Hybrid"); ssc_data_get_number(hybrid_outputs, "project_return_aftertax_npv", &npv); auto ebitda = ssc_data_get_array(hybrid_outputs, "cf_ebitda", &len); + ssc_data_get_number(hybrid_outputs, "annual_energy", &total_energy); auto revenue = ssc_data_get_array(hybrid_outputs, "cf_total_revenue", &len); auto om_expenses = ssc_data_get_array(hybrid_outputs, "cf_operating_expenses", &len); @@ -210,12 +244,17 @@ TEST_F(CmodHybridTest, CustomGenerationPVWattsWindFuelCellBatteryHybrid_SingleOw EXPECT_NEAR(genericannualenergy, 756864000., 1e6); EXPECT_NEAR(pvannualenergy, 211907456., 1e6); EXPECT_NEAR(windannualenergy, 366975552., 1e6); + EXPECT_NEAR(fuelcellannualenergy, 1561993, 1e6); EXPECT_NEAR(battannualenergy, 1331720000., 1e6); + EXPECT_NEAR(battchargeenergy[1], 55372248, 55372248 * 0.001); + EXPECT_NEAR(battdischargeenergy[1], 49818321, 49818321 * 0.001); + EXPECT_NEAR(total_energy, battannualenergy, total_energy * 0.001); + EXPECT_NEAR(total_energy, pvannualenergy + windannualenergy + genericannualenergy + fuelcellannualenergy - battchargeenergy[1] + battdischargeenergy[1], total_energy * 0.001); - EXPECT_NEAR(om_expenses[1], 90570832., 1e5); - EXPECT_NEAR(revenue[1], 66865452., 1e5); - EXPECT_NEAR(ebitda[1], -23705384., 1e5); - EXPECT_NEAR(npv, -1754023822., 1e6); + EXPECT_NEAR(om_expenses[1], 90224679., 1e5); + EXPECT_NEAR(revenue[1], 66590988., 1e5); + EXPECT_NEAR(ebitda[1], -23633690., 1e5); + EXPECT_NEAR(npv, -1750593259., 1e6); } ssc_data_free(dat); dat = nullptr;