diff --git a/shared/lib_utility_rate_equations.h b/shared/lib_utility_rate_equations.h index 3410376f4..c1001a2e0 100644 --- a/shared/lib_utility_rate_equations.h +++ b/shared/lib_utility_rate_equations.h @@ -144,6 +144,8 @@ class rate_data { bool nm_credits_w_rollover; // rate option 0 only int net_metering_credit_month; double nm_credit_sell_rate; + bool nb_credit_expire; // For billing regimes in which credit can be accumulated and spent during a calendar year but cannot be redeemed for cash at the end of the year. + bool nb_apply_credit_current_month; rate_data(); rate_data(const rate_data& tmp); diff --git a/ssc/cmod_utilityrate5.cpp b/ssc/cmod_utilityrate5.cpp index 9e489cdf1..3e5cf3fcc 100644 --- a/ssc/cmod_utilityrate5.cpp +++ b/ssc/cmod_utilityrate5.cpp @@ -415,6 +415,8 @@ void rate_setup::setup(var_table* vt, int num_recs_yearly, size_t nyears, rate_d rate.enable_nm = (metering_option == 0 || metering_option == 1); rate.nm_credits_w_rollover = (vt->as_integer("ur_metering_option") == 0); rate.net_metering_credit_month = (int)vt->as_number("ur_nm_credit_month"); + rate.nb_credit_expire = vt->as_boolean("ur_nb_credit_expire"); + rate.nb_apply_credit_current_month = vt->as_boolean("ur_nb_apply_credit_current_month"); rate.nm_credit_sell_rate = vt->as_number("ur_nm_yearend_sell_rate"); ssc_number_t* ratchet_matrix = NULL; ssc_number_t* bd_tou_matrix = NULL; @@ -2235,6 +2237,10 @@ class cm_utilityrate5 : public compute_module //int metering_option = as_integer("ur_metering_option"); bool excess_monthly_dollars = (as_integer("ur_metering_option") == 3); int excess_dollars_credit_month = (int)as_number("ur_nm_credit_month"); + // Do our credits expire at the end of year + bool nb_credit_expire = as_boolean("ur_nb_credit_expire"); + // Do we wish to apply our credits in the month they are earned + bool nb_apply_credit_current_month = as_boolean("ur_nb_apply_credit_current_month"); rate.tou_demand_single_peak = (as_integer("TOU_demand_single_peak") == 1); @@ -2321,48 +2327,48 @@ class cm_utilityrate5 : public compute_module // main loop c = 0; // hourly count // process one timestep at a time - for (m = 0; m < 12; m++) + for (m = 0; m < 12; m++) // for each month { - ur_month& curr_month = rate.m_month[m]; - monthly_surplus_energy = 0; + ur_month& curr_month = rate.m_month[m]; + monthly_surplus_energy = 0; monthly_deficit_energy = 0; if (ur_ec_hourly_acc_period == 1) { surplus_tier = 0; deficit_tier = 0; } - for (d = 0; d= 0.0) + if (e_in[c] >= 0.0) // If we have money earned { // calculate income or credit e_upper = curr_month.ec_tou_ub.at(row, surplus_tier); // Have to check this each step to swap between surplus and deficit monthly_surplus_energy += e_in[c]; daily_surplus_energy += e_in[c]; // base period charge on units specified - ssc_number_t energy_surplus = e_in[c]; + ssc_number_t energy_surplus = e_in[c]; ssc_number_t cumulative_energy = e_in[c]; - if (ur_ec_hourly_acc_period == 1) + if (ur_ec_hourly_acc_period == 1) // Always 1 cumulative_energy = monthly_surplus_energy; - else if (ur_ec_hourly_acc_period == 2) + else if (ur_ec_hourly_acc_period == 2) // Never taken cumulative_energy = daily_surplus_energy; @@ -2381,15 +2387,15 @@ class cm_utilityrate5 : public compute_module } // Fall back to TOU rates if m_ec_ts_sell_rate.size() is too small - if (tier_credit == 0) { - if (cumulative_energy <= e_upper) { - tier_energy = energy_surplus; + if (tier_credit == 0) { // So AFAICT this is to make sure we don't compute both time step and TOU. Maybe better as an else? + if (cumulative_energy <= e_upper) { // If we are within the max usage for the tier, then it's a simple multiply + tier_energy = energy_surplus; // of our usage with that rate and the escalator sr = curr_month.ec_tou_sr.at(row, surplus_tier); tier_credit = tier_energy * sr * rate_esc; curr_month.ec_energy_surplus.at(row, surplus_tier) += (ssc_number_t)tier_energy; } - else { + else { // Otherwise, we need to handle that the electricity is being sold at different rates bool break_tier_loop = false; while (cumulative_energy > e_upper) { step_surplus_energy = energy_surplus - (cumulative_energy - e_upper); // Subtract amount above the tier to find amount in this tier @@ -2428,7 +2434,7 @@ class cm_utilityrate5 : public compute_module credit_amt = tier_credit; - if (excess_monthly_dollars) + if (excess_monthly_dollars) // NB with carryover to next month { monthly_cumulative_excess_dollars[m] += credit_amt; } @@ -2447,10 +2453,10 @@ class cm_utilityrate5 : public compute_module } } } - else + else // Same calc but with spending money { // calculate payment or charge e_upper = curr_month.ec_tou_ub.at(row, deficit_tier); // Have to check this each step to swap between surplus and deficit - monthly_deficit_energy -= e_in[c]; + monthly_deficit_energy -= e_in[c]; daily_deficit_energy -= e_in[c]; double charge_amt = 0; double energy_deficit = -e_in[c]; @@ -2555,38 +2561,44 @@ class cm_utilityrate5 : public compute_module // Calculate monthly bill (before minimums and fixed charges) and excess kwhs and rollover - monthly_bill[m] = monthly_ec_charges[m] + rate.monthly_dc_fixed[m] + rate.monthly_dc_tou[m]; + monthly_bill[m] = monthly_ec_charges[m] + rate.monthly_dc_fixed[m] + rate.monthly_dc_tou[m]; // This code seems unnecessary, it is repeated verbatim below excess_dollars_earned[m] = monthly_cumulative_excess_dollars[m]; ssc_number_t dollars_applied = 0; // apply previous month rollover kwhs - if (excess_monthly_dollars) + if (excess_monthly_dollars) // If in net billing with carrover to next month { // Overwrite this to include the current month's charges monthly_ec_charges_gross[m] = monthly_ec_charges[m]; - if (m == 0 && excess_dollars_credit_month != 11) { - monthly_ec_charges[m] -= prev_excess_dollars; + if (m == 0 && excess_dollars_credit_month != 11) { // If start of the year and the rollover month not december + monthly_ec_charges[m] -= prev_excess_dollars; // prev_excess dollars is the value of the credit coming in from last year payment[c - 1] -= prev_excess_dollars; dollars_applied += prev_excess_dollars; } - else if (m > 0 && m != excess_dollars_credit_month + 1) + else if (m > 0 && m != excess_dollars_credit_month + 1) // If during the middle of the year and the rollover month not the prior month { monthly_ec_charges[m] -= monthly_cumulative_excess_dollars[m - 1]; payment[c - 1] -= monthly_cumulative_excess_dollars[m - 1]; dollars_applied += monthly_cumulative_excess_dollars[m - 1]; } + if (nb_apply_credit_current_month) { + monthly_ec_charges[m] -= monthly_cumulative_excess_dollars[m]; + payment[c - 1] -= monthly_cumulative_excess_dollars[m]; + dollars_applied += monthly_cumulative_excess_dollars[m]; + monthly_cumulative_excess_dollars[m] = 0; + } // Rollover credits at end of true-up period are applied after annual minimums below this section - if (monthly_ec_charges[m] < 0) + if (monthly_ec_charges[m] < 0) // If this brings the monthly energy charges negative, then { - payment[c - 1] -= monthly_ec_charges[m]; - dollars_applied += monthly_ec_charges[m]; - monthly_cumulative_excess_dollars[m] -= monthly_ec_charges[m]; + payment[c - 1] -= monthly_ec_charges[m]; // Add the magnitude of the negative amount to the last hour of the month + dollars_applied += monthly_ec_charges[m];// Subtract the magnitude of the negative amount from the dollars applied + monthly_cumulative_excess_dollars[m] -= monthly_ec_charges[m]; //add in the magnitude of the negative amount to the cumulative excess for this month monthly_ec_charges[m] = 0; } - + // I'm pretty sure this branch can't be taken. if (monthly_ec_charges_gross[m] < dollars_applied) dollars_applied = monthly_ec_charges_gross[m]; net_billing_credits[m] = dollars_applied; } @@ -2645,17 +2657,19 @@ class cm_utilityrate5 : public compute_module } } } - - if (m == excess_dollars_credit_month) - { - // apply annual rollovers AFTER minimum calculations - if (excess_monthly_dollars && (monthly_cumulative_excess_dollars[m] > 0)) - { - income[c] += monthly_cumulative_excess_dollars[m]; - monthly_bill[m] -= monthly_cumulative_excess_dollars[m]; - monthly_true_up_credits[m] = monthly_cumulative_excess_dollars[m]; - } - } + if (!nb_credit_expire) + { + if (m == excess_dollars_credit_month) + { + // apply annual rollovers AFTER minimum calculations + if (excess_monthly_dollars && (monthly_cumulative_excess_dollars[m] > 0)) + { + income[c] += monthly_cumulative_excess_dollars[m]; + monthly_bill[m] -= monthly_cumulative_excess_dollars[m]; + monthly_true_up_credits[m] = monthly_cumulative_excess_dollars[m]; + } + } + } monthly_bill[m] += monthly_fixed_charges[m] + monthly_minimum_charges[m]; } revenue[c] = income[c] - payment[c]; diff --git a/ssc/common.cpp b/ssc/common.cpp index b12b1eae3..baaf04f0d 100644 --- a/ssc/common.cpp +++ b/ssc/common.cpp @@ -1010,7 +1010,8 @@ var_info vtab_utility_rate_common[] = { { SSC_INPUT, SSC_NUMBER, "ur_nm_credit_month", "Month of year end payout (true-up)", "mn", "", "Electricity Rates", "?=11", "INTEGER,MIN=0,MAX=11", "" }, { SSC_INPUT, SSC_NUMBER, "ur_nm_credit_rollover", "Apply net metering true-up credits to future bills", "0/1", "0=disable,1=enable", "Electricity Rates", "?=0", "INTEGER,MIN=0,MAX=1", "" }, { SSC_INPUT, SSC_NUMBER, "ur_monthly_fixed_charge", "Monthly fixed charge", "$", "", "Electricity Rates", "?=0.0", "", "" }, - + { SSC_INPUT, SSC_NUMBER, "ur_nb_credit_expire", "Credit is lost upon end of year ", "0/1", "0=disable,1=enable", "Electricity Rates", "?=0", "INTEGER,MIN=0,MAX=1", "" }, + { SSC_INPUT, SSC_NUMBER, "ur_nb_apply_credit_current_month", "Apply earned credits to balance before rolling over excess ", "0/1", "0=disable,1=enable", "Electricity Rates", "?=0", "INTEGER,MIN=0,MAX=1", "" }, // optional input that allows sell rates to be overridden with buy rates - defaults to not override { SSC_INPUT, SSC_NUMBER, "ur_sell_eq_buy", "Set sell rate equal to buy rate", "0/1", "Optional override", "Electricity Rates", "?=0", "BOOLEAN", "" }, diff --git a/test/ssc_test/cmod_utilityrate5_test.cpp b/test/ssc_test/cmod_utilityrate5_test.cpp index 030f395c0..1e9da2184 100644 --- a/test/ssc_test/cmod_utilityrate5_test.cpp +++ b/test/ssc_test/cmod_utilityrate5_test.cpp @@ -1999,3 +1999,179 @@ TEST(cmod_utilityrate5_eqns, Test_Residential_TOU_Rates_net_billing_w_tiers_buy_ } +TEST(cmod_utilityrate5_eqns, Test_Residential_TOU_Rates_net_billing_carryover_inflow_outflow_expire) { + ssc_data_t data = new var_table; + + setup_residential_rates(data); + ssc_data_set_number(data, "ur_metering_option", 3); + ssc_number_t p_ur_ec_tou_mat[24] = { 1, 1, 9.9999999999999998e+37, 0, 0.10000000000000001, 0.10000000000000001, + 2, 1, 9.9999999999999998e+37, 0, 0.050000000000000003, 0.050000000000000003, + 3, 1, 9.9999999999999998e+37, 0, 0.20000000000000001, 0.20000000000000001, + 4, 1, 9.9999999999999998e+37, 0, 0.25, 0.25 }; + ssc_data_set_matrix(data, "ur_ec_tou_mat", p_ur_ec_tou_mat, 4, 6); + ssc_data_set_number(data, "ur_nb_credit_expire", 1); + ssc_data_set_number(data, "ur_nb_apply_credit_current_month", 1); + + int analysis_period = 25; + ssc_data_set_number(data, "system_use_lifetime_output", 1); + ssc_data_set_number(data, "analysis_period", analysis_period); + set_array(data, "load", load_profile_path, 8760); + set_array(data, "gen", gen_path, 8760 * analysis_period); + + int status = run_module(data, "utilityrate5"); + EXPECT_FALSE(status); + + ensure_outputs_line_up(data); + + ssc_number_t cost_without_system; + ssc_data_get_number(data, "elec_cost_without_system_year1", &cost_without_system); + EXPECT_NEAR(771.8, cost_without_system, 0.1); + + ssc_number_t cost_with_system; + ssc_data_get_number(data, "elec_cost_with_system_year1", &cost_with_system); + EXPECT_NEAR(0.0, cost_with_system, 0.01); + + int nrows; + int ncols; + ssc_number_t* annual_bills = ssc_data_get_matrix(data, "utility_bill_w_sys_ym", &nrows, &ncols); + util::matrix_t bill_matrix(nrows, ncols); + bill_matrix.assign(annual_bills, nrows, ncols); + + double jan_year_2 = bill_matrix.at((size_t)2, (size_t)0); + EXPECT_NEAR(0.0, jan_year_2, 0.1); + + ssc_number_t* true_up_credits = ssc_data_get_matrix(data, "true_up_credits_ym", &nrows, &ncols); + util::matrix_t true_up_credits_matrix(nrows, ncols); + true_up_credits_matrix.assign(true_up_credits, nrows, ncols); + + double dec_year_1_credits = true_up_credits_matrix.at((size_t)1, (size_t)11); + EXPECT_NEAR(0.0, dec_year_1_credits, 0.1); + + ssc_number_t* net_billing_credits = ssc_data_get_matrix(data, "net_billing_credits_ym", &nrows, &ncols); + util::matrix_t credits_matrix(nrows, ncols); + credits_matrix.assign(net_billing_credits, nrows, ncols); + + double jan_year_2_credits = credits_matrix.at((size_t)2, (size_t)0); + EXPECT_NEAR(32.55, jan_year_2_credits, 0.1); + + ssc_data_free(data); + +} + +TEST(cmod_utilityrate5_eqns, Test_Residential_TOU_Rates_net_billing_carryover_inflow_outflow_apply_no_expire) { + ssc_data_t data = new var_table; + + setup_residential_rates(data); + ssc_data_set_number(data, "ur_metering_option", 3); + ssc_number_t p_ur_ec_tou_mat[24] = { 1, 1, 9.9999999999999998e+37, 0, 0.10000000000000001, 0.10000000000000001, + 2, 1, 9.9999999999999998e+37, 0, 0.050000000000000003, 0.050000000000000003, + 3, 1, 9.9999999999999998e+37, 0, 0.20000000000000001, 0.20000000000000001, + 4, 1, 9.9999999999999998e+37, 0, 0.25, 0.25 }; + ssc_data_set_matrix(data, "ur_ec_tou_mat", p_ur_ec_tou_mat, 4, 6); + ssc_data_set_number(data, "ur_nb_credit_expire", 0); + ssc_data_set_number(data, "ur_nb_apply_credit_current_month", 1); + + int analysis_period = 25; + ssc_data_set_number(data, "system_use_lifetime_output", 1); + ssc_data_set_number(data, "analysis_period", analysis_period); + set_array(data, "load", load_profile_path, 8760); + set_array(data, "gen", gen_path, 8760 * analysis_period); + + int status = run_module(data, "utilityrate5"); + EXPECT_FALSE(status); + + ensure_outputs_line_up(data); + + ssc_number_t cost_without_system; + ssc_data_get_number(data, "elec_cost_without_system_year1", &cost_without_system); + EXPECT_NEAR(771.8, cost_without_system, 0.1); + + ssc_number_t cost_with_system; + ssc_data_get_number(data, "elec_cost_with_system_year1", &cost_with_system); + EXPECT_NEAR(-150.02, cost_with_system, 0.01); + + int nrows; + int ncols; + ssc_number_t* annual_bills = ssc_data_get_matrix(data, "utility_bill_w_sys_ym", &nrows, &ncols); + util::matrix_t bill_matrix(nrows, ncols); + bill_matrix.assign(annual_bills, nrows, ncols); + + double jan_year_2 = bill_matrix.at((size_t)2, (size_t)0); + EXPECT_NEAR(0, jan_year_2, 0.1); + + ssc_number_t* true_up_credits = ssc_data_get_matrix(data, "true_up_credits_ym", &nrows, &ncols); + util::matrix_t true_up_credits_matrix(nrows, ncols); + true_up_credits_matrix.assign(true_up_credits, nrows, ncols); + + double dec_year_1_credits = true_up_credits_matrix.at((size_t)1, (size_t)11); + EXPECT_NEAR(150.02, dec_year_1_credits, 0.1); + + ssc_number_t* net_billing_credits = ssc_data_get_matrix(data, "net_billing_credits_ym", &nrows, &ncols); + util::matrix_t credits_matrix(nrows, ncols); + credits_matrix.assign(net_billing_credits, nrows, ncols); + + double jan_year_2_credits = credits_matrix.at((size_t)2, (size_t)0); + EXPECT_NEAR(32.54, jan_year_2_credits, 0.1); + + ssc_data_free(data); + +} + +TEST(cmod_utilityrate5_eqns, Test_Residential_TOU_Rates_net_billing_carryover_inflow_outflow_expire_no_apply) { + ssc_data_t data = new var_table; + + setup_residential_rates(data); + ssc_data_set_number(data, "ur_metering_option", 3); + ssc_number_t p_ur_ec_tou_mat[24] = { 1, 1, 9.9999999999999998e+37, 0, 0.10000000000000001, 0.10000000000000001, + 2, 1, 9.9999999999999998e+37, 0, 0.050000000000000003, 0.050000000000000003, + 3, 1, 9.9999999999999998e+37, 0, 0.20000000000000001, 0.20000000000000001, + 4, 1, 9.9999999999999998e+37, 0, 0.25, 0.25 }; + ssc_data_set_matrix(data, "ur_ec_tou_mat", p_ur_ec_tou_mat, 4, 6); + ssc_data_set_number(data, "ur_nb_credit_expire", 0); + ssc_data_set_number(data, "ur_nb_apply_credit_current_month", 1); + + int analysis_period = 25; + ssc_data_set_number(data, "system_use_lifetime_output", 1); + ssc_data_set_number(data, "analysis_period", analysis_period); + set_array(data, "load", load_profile_path, 8760); + set_array(data, "gen", gen_path, 8760 * analysis_period); + + int status = run_module(data, "utilityrate5"); + EXPECT_FALSE(status); + + ensure_outputs_line_up(data); + + ssc_number_t cost_without_system; + ssc_data_get_number(data, "elec_cost_without_system_year1", &cost_without_system); + EXPECT_NEAR(771.8, cost_without_system, 0.1); + + ssc_number_t cost_with_system; + ssc_data_get_number(data, "elec_cost_with_system_year1", &cost_with_system); + EXPECT_NEAR(-150.02, cost_with_system, 0.01); + + int nrows; + int ncols; + ssc_number_t* annual_bills = ssc_data_get_matrix(data, "utility_bill_w_sys_ym", &nrows, &ncols); + util::matrix_t bill_matrix(nrows, ncols); + bill_matrix.assign(annual_bills, nrows, ncols); + + double jan_year_2 = bill_matrix.at((size_t)2, (size_t)0); + EXPECT_NEAR(0.0, jan_year_2, 0.1); + + ssc_number_t* true_up_credits = ssc_data_get_matrix(data, "true_up_credits_ym", &nrows, &ncols); + util::matrix_t true_up_credits_matrix(nrows, ncols); + true_up_credits_matrix.assign(true_up_credits, nrows, ncols); + + double dec_year_1_credits = true_up_credits_matrix.at((size_t)1, (size_t)11); + EXPECT_NEAR(150.02, dec_year_1_credits, 0.1); + + ssc_number_t* net_billing_credits = ssc_data_get_matrix(data, "net_billing_credits_ym", &nrows, &ncols); + util::matrix_t credits_matrix(nrows, ncols); + credits_matrix.assign(net_billing_credits, nrows, ncols); + + double jan_year_2_credits = credits_matrix.at((size_t)2, (size_t)0); + EXPECT_NEAR(32.55, jan_year_2_credits, 0.1); + + ssc_data_free(data); + +}