From 94b1b1c8aa73db81f95f8b21f8979c84535c04a9 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 29 Feb 2024 10:40:34 -0700 Subject: [PATCH 01/22] add wake model enum for clarity --- ssc/cmod_windpower.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 3ca4b5038..31d5ce12e 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -39,6 +39,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "lib_util.h" #include "cmod_windpower.h" +enum wakeModelOptions { SIMPLE, PARK, EDDYVISCOSITY, CONSTANTVALUE }; + static var_info _cm_vtab_windpower[] = { // VARTYPE DATATYPE NAME LABEL UNITS META GROUP REQUIRED_IF CONSTRAINTS UI_HINTS { SSC_INPUT , SSC_NUMBER , "wind_resource_model_choice" , "Hourly, Weibull or Distribution model" , "0/1/2" ,"" , "Resource" , "*" , "INTEGER" , "" } , @@ -384,16 +386,16 @@ void cm_windpower::exec() // create wakeModel std::shared_ptr wakeModel(nullptr); int wakeModelChoice = as_integer("wind_farm_wake_model"); - if (wakeModelChoice == 0) + if (wakeModelChoice == SIMPLE) wakeModel = std::make_shared(simpleWakeModel(wpc.nTurbines, &wt)); - else if (wakeModelChoice == 1) + else if (wakeModelChoice == PARK) wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt)); - else if (wakeModelChoice == 2) + else if (wakeModelChoice == EDDYVISCOSITY) { wpc.turbulenceIntensity *= 100; wakeModel = std::make_shared(eddyViscosityWakeModel(wpc.nTurbines, &wt, as_double("wind_resource_turbulence_coeff"))); } - else if (wakeModelChoice == 3) + else if (wakeModelChoice == CONSTANTVALUE) { wake_int_loss_percent = as_double("wake_int_loss"); wakeModel = std::make_shared(constantWakeModel(wpc.nTurbines, &wt, (100. - wake_int_loss_percent)/100.)); @@ -412,7 +414,7 @@ void cm_windpower::exec() throw exec_error("windpower", wpc.GetErrorDetails()); } - if (wakeModelChoice != 3) + if (wakeModelChoice != CONSTANTVALUE) wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; int nstep = 8760; @@ -652,7 +654,7 @@ void cm_windpower::exec() assign("wind_speed_average", wsp_avg); // internal wake loss is calculated during simulation rather than provided - if (wakeModelChoice != 3){ + if (wakeModelChoice != CONSTANTVALUE){ wake_int_loss_percent = (1. - annual_after_wake_loss/annual_gross) * 100.; } From 7e3082eed4b65ca707c32ba81bbf6720e89c8afb Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 29 Feb 2024 11:48:28 -0700 Subject: [PATCH 02/22] add timeseries and additional annual wake loss outputs to the wind model, renamed wake loss variables and outputs for clarity --- ssc/cmod_windpower.cpp | 63 ++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 31d5ce12e..d29be6d06 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -94,32 +94,39 @@ static var_info _cm_vtab_windpower[] = { // OUTPUTS ----------------------------------------------------------------------------annual_energy - { SSC_OUTPUT , SSC_ARRAY , "turbine_output_by_windspeed_bin" , "Turbine output by wind speed bin" , "kW" ,"" , "Power Curve" ,"" , "LENGTH_EQUAL=wind_turbine_powercurve_windspeeds" , "" } , - { SSC_OUTPUT , SSC_ARRAY , "wind_direction" , "Wind direction" , "degrees" ,"" , "Time Series" , "wind_resource_model_choice=0" , "" , "" } , + // weather data outputs + { SSC_OUTPUT , SSC_ARRAY , "turbine_output_by_windspeed_bin" , "Turbine output by wind speed bin" , "kW" ,"" , "Power Curve" ,"" , "LENGTH_EQUAL=wind_turbine_powercurve_windspeeds" , "" } , + { SSC_OUTPUT , SSC_ARRAY , "wind_direction" , "Wind direction" , "degrees" ,"" , "Time Series" , "wind_resource_model_choice=0" , "" , "" } , { SSC_OUTPUT , SSC_ARRAY , "wind_speed" , "Wind speed" , "m/s" ,"" , "Time Series" , "wind_resource_model_choice=0" , "" , "" } , { SSC_OUTPUT , SSC_ARRAY , "temp" , "Air temperature" , "'C" ,"" , "Time Series" , "wind_resource_model_choice=0" , "" , "" } , { SSC_OUTPUT , SSC_ARRAY , "pressure" , "Pressure" , "atm" ,"" , "Time Series" , "wind_resource_model_choice=0" , "" , "" } , // pass through weather file header data to outputs - { SSC_OUTPUT , SSC_NUMBER , "lat" , "Latitude" , "degrees" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "lon" , "Longitude" , "degrees" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "elev" , "Site elevation" , "m" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "year" , "Year" , "" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "lat" , "Latitude" , "degrees" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "lon" , "Longitude" , "degrees" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "elev" , "Site elevation" , "m" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "year" , "Year" , "" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , - { SSC_OUTPUT , SSC_ARRAY , "monthly_energy" , "Monthly Energy Gross" , "kWh" ,"" , "Monthly" , "*" , "LENGTH=12" , "" } , + // timeseries outputs + { SSC_OUTPUT , SSC_ARRAY , "timeseries_wake_loss_kW" , "Wake loss at each timestep in kW" , "kW" ,"" , "Time Series" , "" , "" , "" } , + { SSC_OUTPUT , SSC_ARRAY , "timeseries_wake_loss_percent" , "Wake loss at each timestep percent" , "%" ,"" , "Time Series" , "" , "" , "" } , + + // monthly and annual outputs + { SSC_OUTPUT , SSC_ARRAY , "monthly_energy" , "Monthly Energy Gross" , "kWh" ,"" , "Monthly" , "*" , "LENGTH=12" , "" } , { SSC_OUTPUT , SSC_NUMBER , "annual_energy" , "Annual Energy" , "kWh" ,"" , "Annual" , "*" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "annual_gross_energy" , "Annual Gross Energy" , "kWh" ,"" , "Annual" , "*" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "capacity_factor" , "Capacity factor" , "%" ,"" , "Annual" , "*" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "kwh_per_kw" , "First year kWh/kW" , "kWh/kW" ,"" , "Annual" , "*" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "wind_speed_average" , "Average Wind speed" , "m/s" ,"" , "Annual" , "" , "" , "" } , + // loss outputs { SSC_OUTPUT , SSC_NUMBER , "avail_losses" , "Availability losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "elec_losses" , "Electrical losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "env_losses" , "Environmental losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "ops_losses" , "Operational losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "turb_losses" , "Turbine losses" , "%" ,"" , "Annual" ,"" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "wake_losses" , "Wake losses" , "%" ,"" , "Annual" ,"" , "" , "" } , - + { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_percent" , "Annual wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_kWh" , "Annual wake loss" , "kWh" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "cutoff_losses" , "Low temp and Icing Cutoff losses" , "%" ,"" , "Annual" ,"" , "" , "" } , var_info_invalid }; @@ -233,7 +240,7 @@ cm_windpower::cm_windpower(){ } // wind PRUF loss framework. Can replace numerical loss percentages by calculated losses in future model -void calculate_losses(compute_module *cm, double wake_int_loss_percent) { +void calculate_losses(compute_module *cm, double annual_wake_int_loss_percent) { double avail_loss_percent = 1. - ( 100. - cm->as_double("avail_bop_loss"))/100. * (100. - cm->as_double("avail_grid_loss"))/100. * ( 100. - cm->as_double("avail_turb_loss"))/100.; double elec_loss_percent = 1. - ( 100. - cm->as_double("elec_eff_loss"))/100. * ( 100. - cm->as_double("elec_parasitic_loss"))/100.; @@ -245,13 +252,13 @@ void calculate_losses(compute_module *cm, double wake_int_loss_percent) { double turb_loss_percent = 1. - ( 100. - cm->as_double("turb_generic_loss"))/100. * ( 100. - cm->as_double("turb_hysteresis_loss"))/100. * ( 100. - cm->as_double("turb_perf_loss"))/100. * ( 100. - cm->as_double("turb_specific_loss"))/100.; double wake_loss_percent = 1. - ( 100. - cm->as_double("wake_ext_loss"))/100. * ( 100. - cm->as_double("wake_future_loss"))/100. - * (100. - wake_int_loss_percent) / 100.; + * (100. - annual_wake_int_loss_percent) / 100.; cm->assign("avail_losses", avail_loss_percent * 100.); cm->assign("elec_losses", elec_loss_percent * 100.); cm->assign("env_losses", env_loss_percent * 100.); cm->assign("ops_losses", ops_loss_percent * 100.); cm->assign("turb_losses", turb_loss_percent * 100.); - cm->assign("wake_losses", wake_loss_percent * 100.); + cm->assign("annual_wake_loss_percent", wake_loss_percent * 100.); } double get_fixed_losses(compute_module* cm){ @@ -325,7 +332,7 @@ void cm_windpower::exec() if (lossMultiplier > 1 || lossMultiplier < 0){ throw exec_error("windpower", "Total percent losses must be between 0 and 100."); } - double wake_int_loss_percent = 0.; + double annual_wake_int_loss_percent = 0.; bool lowTempCutoff = as_boolean("en_low_temp_cutoff"); double lowTempCutoffValue = lowTempCutoff ? as_double("low_temp_cutoff") : -1; @@ -351,8 +358,8 @@ void cm_windpower::exec() double turbine_kw = wpc.windPowerUsingWeibull(weibull_k, avg_speed, ref_height, &turbine_outkW[0]); ssc_number_t gross_energy = turbine_kw * wpc.nTurbines; - wake_int_loss_percent = is_assigned("wake_int_loss") ? as_double("wake_int_loss") : 0.; - turbine_kw = turbine_kw * lossMultiplier * (1. - wake_int_loss_percent/100.); + annual_wake_int_loss_percent = is_assigned("wake_int_loss") ? as_double("wake_int_loss") : 0.; + turbine_kw = turbine_kw * lossMultiplier * (1. - annual_wake_int_loss_percent/100.); int nstep = 8760; ssc_number_t farm_kw = (ssc_number_t)turbine_kw * wpc.nTurbines / (ssc_number_t)nstep; @@ -379,7 +386,7 @@ void cm_windpower::exec() assign("annual_gross_energy", gross_energy); assign("wind_speed", avg_speed); calculate_p50p90(this); - calculate_losses(this, wake_int_loss_percent); + calculate_losses(this, annual_wake_int_loss_percent); return; } @@ -397,8 +404,8 @@ void cm_windpower::exec() } else if (wakeModelChoice == CONSTANTVALUE) { - wake_int_loss_percent = as_double("wake_int_loss"); - wakeModel = std::make_shared(constantWakeModel(wpc.nTurbines, &wt, (100. - wake_int_loss_percent)/100.)); + annual_wake_int_loss_percent = as_double("wake_int_loss"); + wakeModel = std::make_shared(constantWakeModel(wpc.nTurbines, &wt, (100. - annual_wake_int_loss_percent)/100.)); } else{ throw exec_error("windpower", util::format("wind_farm_wake_model must be 0, 1, 2 or 3.")); @@ -415,7 +422,7 @@ void cm_windpower::exec() } if (wakeModelChoice != CONSTANTVALUE) - wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; + annual_wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; int nstep = 8760; ssc_number_t farm_kw = farmPower / (ssc_number_t)nstep; @@ -446,7 +453,7 @@ void cm_windpower::exec() assign("annual_gross_energy", farmPowerGross); assign("wind_speed_average", avg_speed); calculate_p50p90(this); - calculate_losses(this, wake_int_loss_percent); + calculate_losses(this, annual_wake_int_loss_percent); return; } @@ -510,6 +517,8 @@ void cm_windpower::exec() // allocate output data ssc_number_t *farmpwr = allocate("gen", nstep); + ssc_number_t* wakeLosskW = allocate("timeseries_wake_loss_kW", nstep); + ssc_number_t* wakeLossPercent = allocate("timeseries_wake_loss_percent", nstep); ssc_number_t *wspd = allocate("wind_speed", nstep); ssc_number_t *wdir = allocate("wind_direction", nstep); ssc_number_t *air_temp = allocate("temp", nstep); @@ -604,8 +613,13 @@ void cm_windpower::exec() &DistCross[0])) throw exec_error("windpower", util::format("error in wind calculation at time %d, details: %s", i, wpc.GetErrorDetails().c_str())); - annual_gross += gross_farmp; - annual_after_wake_loss += farmp; + //wake loss calculations need to happen before other losses are applied + annual_gross += gross_farmp; //i don't think this works for subhourly data???????????????? + annual_after_wake_loss += farmp; + wakeLosskW[i] = gross_farmp - farmp; + if (gross_farmp == 0.0) wakeLossPercent[i] = 0.0; + else wakeLossPercent[i] = wakeLosskW[i] / gross_farmp * 100.0; + farmp *= lossMultiplier; // apply and track cutoff losses withoutCutOffLosses += farmp * haf(hr); @@ -655,11 +669,12 @@ void cm_windpower::exec() // internal wake loss is calculated during simulation rather than provided if (wakeModelChoice != CONSTANTVALUE){ - wake_int_loss_percent = (1. - annual_after_wake_loss/annual_gross) * 100.; + annual_wake_int_loss_percent = (1. - annual_after_wake_loss/annual_gross) * 100.; + assign("annual_wake_loss_kWh", var_data((ssc_number_t)(annual_gross - annual_after_wake_loss))); } calculate_p50p90(this); - calculate_losses(this, wake_int_loss_percent); + calculate_losses(this, annual_wake_int_loss_percent); } // exec DEFINE_MODULE_ENTRY(windpower, "Utility scale wind farm model (adapted from TRNSYS code by P.Quinlan and openWind software by AWS Truepower)", 2); From 927bbf25763bd580c5f43ad9c8f3c950be7e5b62 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Sat, 2 Mar 2024 13:43:26 -0700 Subject: [PATCH 03/22] updated some annual energy outputs in wind model, fixes #1143 --- ssc/cmod_windpower.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index d29be6d06..8fc11b54d 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -532,10 +532,10 @@ void cm_windpower::exec() ssc_number_t *monthly = allocate("monthly_energy", 12); for (int i = 0; i < 12; i++) monthly[i] = 0.0f; - double annual = 0.0; - double annual_gross = 0.0; - double withoutCutOffLosses = 0.0; - double annual_after_wake_loss = 0.0; + double annual = 0.0; //final annual energy in kWh + double annual_gross = 0.0; //annual energy before any losses in kWh + double withoutCutOffLosses = 0.0; //annual energy without low temperature/icing cutoff losses applied in kWh + double annual_after_wake_loss = 0.0; //annual energy after wake losses but before other losses in kWh // compute power output at i-th timestep int i = 0; @@ -614,15 +614,15 @@ void cm_windpower::exec() throw exec_error("windpower", util::format("error in wind calculation at time %d, details: %s", i, wpc.GetErrorDetails().c_str())); //wake loss calculations need to happen before other losses are applied - annual_gross += gross_farmp; //i don't think this works for subhourly data???????????????? - annual_after_wake_loss += farmp; + annual_gross += gross_farmp / (ssc_number_t)steps_per_hour; + annual_after_wake_loss += farmp / (ssc_number_t)steps_per_hour; wakeLosskW[i] = gross_farmp - farmp; if (gross_farmp == 0.0) wakeLossPercent[i] = 0.0; else wakeLossPercent[i] = wakeLosskW[i] / gross_farmp * 100.0; farmp *= lossMultiplier; // apply and track cutoff losses - withoutCutOffLosses += farmp * haf(hr); + withoutCutOffLosses += farmp * haf(hr) / (ssc_number_t)steps_per_hour; if (lowTempCutoff){ if (temp < lowTempCutoffValue) farmp = 0.0; } From 52e941cb9968b7494cee54a10ce164cd8b6d4578 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Wed, 6 Mar 2024 10:02:35 -0700 Subject: [PATCH 04/22] update annual wake loss outputs in wind model to be more clear and provide additional values --- ssc/cmod_windpower.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 8fc11b54d..4507f6d25 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -70,6 +70,7 @@ static var_info _cm_vtab_windpower[] = { { SSC_INPUT , SSC_NUMBER , "en_icing_cutoff" , "Enable Icing Cutoff" , "0/1" ,"" , "Losses" , "?=0" , "INTEGER" , "" } , { SSC_INPUT , SSC_NUMBER , "icing_cutoff_temp" , "Icing Cutoff Temperature" , "C" ,"" , "Losses" , "en_icing_cutoff=1" , "" , "" } , { SSC_INPUT , SSC_NUMBER , "icing_cutoff_rh" , "Icing Cutoff Relative Humidity" , "%" ,"'rh' required in wind_resource_data" , "Losses" , "en_icing_cutoff=1" , "MIN=0" , "" } , + { SSC_INPUT , SSC_NUMBER , "wake_int_loss" , "Constant Wake Model, internal wake loss" , "%" ,"" , "Losses" , "wind_farm_wake_model=3" , "MIN=0,MAX=100" , "" } , { SSC_INPUT , SSC_NUMBER , "wake_ext_loss" , "External Wake loss" , "%" ,"" , "Losses" , "?=0" , "MIN=0,MAX=100" , "" } , { SSC_INPUT , SSC_NUMBER , "wake_future_loss" , "Future Wake loss" , "%" ,"" , "Losses" , "?=0" , "MIN=0,MAX=100" , "" } , @@ -125,8 +126,9 @@ static var_info _cm_vtab_windpower[] = { { SSC_OUTPUT , SSC_NUMBER , "env_losses" , "Environmental losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "ops_losses" , "Operational losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "turb_losses" , "Turbine losses" , "%" ,"" , "Annual" ,"" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_percent" , "Annual wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_kWh" , "Annual wake loss" , "kWh" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_internal_wake_loss_percent" , "Annual internal wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_internal_wake_loss_kWh" , "Annual internal wake loss" , "kWh" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_total_wake_loss_percent" , "Annual total wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "cutoff_losses" , "Low temp and Icing Cutoff losses" , "%" ,"" , "Annual" ,"" , "" , "" } , var_info_invalid }; @@ -240,6 +242,7 @@ cm_windpower::cm_windpower(){ } // wind PRUF loss framework. Can replace numerical loss percentages by calculated losses in future model +// annual_wake_int_loss_percent is the annual INTERNAL wake loss, either calculated or specified by user void calculate_losses(compute_module *cm, double annual_wake_int_loss_percent) { double avail_loss_percent = 1. - ( 100. - cm->as_double("avail_bop_loss"))/100. * (100. - cm->as_double("avail_grid_loss"))/100. * ( 100. - cm->as_double("avail_turb_loss"))/100.; @@ -251,14 +254,14 @@ void calculate_losses(compute_module *cm, double annual_wake_int_loss_percent) { * ( 100. - cm->as_double("ops_load_loss"))/100. * ( 100. - cm->as_double("ops_strategies_loss"))/100.; double turb_loss_percent = 1. - ( 100. - cm->as_double("turb_generic_loss"))/100. * ( 100. - cm->as_double("turb_hysteresis_loss"))/100. * ( 100. - cm->as_double("turb_perf_loss"))/100. * ( 100. - cm->as_double("turb_specific_loss"))/100.; - double wake_loss_percent = 1. - ( 100. - cm->as_double("wake_ext_loss"))/100. * ( 100. - cm->as_double("wake_future_loss"))/100. + double total_wake_loss_percent = 1. - ( 100. - cm->as_double("wake_ext_loss"))/100. * ( 100. - cm->as_double("wake_future_loss"))/100. * (100. - annual_wake_int_loss_percent) / 100.; cm->assign("avail_losses", avail_loss_percent * 100.); cm->assign("elec_losses", elec_loss_percent * 100.); cm->assign("env_losses", env_loss_percent * 100.); cm->assign("ops_losses", ops_loss_percent * 100.); cm->assign("turb_losses", turb_loss_percent * 100.); - cm->assign("annual_wake_loss_percent", wake_loss_percent * 100.); + cm->assign("annual_total_wake_loss_percent", total_wake_loss_percent * 100.); } double get_fixed_losses(compute_module* cm){ @@ -332,7 +335,7 @@ void cm_windpower::exec() if (lossMultiplier > 1 || lossMultiplier < 0){ throw exec_error("windpower", "Total percent losses must be between 0 and 100."); } - double annual_wake_int_loss_percent = 0.; + double annual_wake_int_loss_percent = 0.; //this is the wake loss due to INTERNAL wakes, either calculated or specified by user depending on the wake model chosen bool lowTempCutoff = as_boolean("en_low_temp_cutoff"); double lowTempCutoffValue = lowTempCutoff ? as_double("low_temp_cutoff") : -1; @@ -422,7 +425,11 @@ void cm_windpower::exec() } if (wakeModelChoice != CONSTANTVALUE) + { + assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); annual_wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; + assign("annual_internal_wake_loss_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); + } int nstep = 8760; ssc_number_t farm_kw = farmPower / (ssc_number_t)nstep; @@ -669,8 +676,9 @@ void cm_windpower::exec() // internal wake loss is calculated during simulation rather than provided if (wakeModelChoice != CONSTANTVALUE){ + assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(annual_gross - annual_after_wake_loss))); annual_wake_int_loss_percent = (1. - annual_after_wake_loss/annual_gross) * 100.; - assign("annual_wake_loss_kWh", var_data((ssc_number_t)(annual_gross - annual_after_wake_loss))); + assign("annual_internal_wake_loss_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); } calculate_p50p90(this); From 3e2b820e726532ed20f49ee3381cfaef95b864cd Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Wed, 6 Mar 2024 11:48:50 -0700 Subject: [PATCH 05/22] update wind tests for new wake loss output names --- ssc/cmod_windpower.cpp | 15 ++++++++++++++- test/ssc_test/cmod_windpower_test.cpp | 8 ++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 4507f6d25..ed35289e3 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -59,7 +59,7 @@ static var_info _cm_vtab_windpower[] = { { SSC_INPUT , SSC_NUMBER , "wind_turbine_max_cp" , "Max Coefficient of Power" , "" ,"" , "Turbine" , "wind_resource_model_choice=1" , "MIN=0" , "" } , { SSC_INPUT , SSC_NUMBER , "wind_farm_wake_model" , "Wake Model [Simple, Park, EV, Constant]" , "0/1/2/3" ,"" , "Farm" , "*" , "INTEGER" , "" } , - { SSC_INPUT , SSC_NUMBER , "wind_resource_turbulence_coeff" , "Turbulence coefficient" , "%" ,"" , "Farm" , "*" , "MIN=0" , "" } , + { SSC_INPUT , SSC_NUMBER , "wind_resource_turbulence_coeff" , "Turbulence coefficient" , "%" ,"" , "Farm" , "*" , "MIN=0" , "" } , { SSC_INPUT , SSC_NUMBER , "system_capacity" , "Nameplate capacity" , "kW" ,"" , "Farm" , "*" , "MIN=0" , "" } , { SSC_INPUT , SSC_ARRAY , "wind_farm_xCoordinates" , "Turbine X coordinates" , "m" ,"" , "Farm" , "*" , "" , "" } , { SSC_INPUT , SSC_ARRAY , "wind_farm_yCoordinates" , "Turbine Y coordinates" , "m" ,"" , "Farm" , "*" , "LENGTH_EQUAL=wind_farm_xCoordinates" , "" } , @@ -71,6 +71,8 @@ static var_info _cm_vtab_windpower[] = { { SSC_INPUT , SSC_NUMBER , "icing_cutoff_temp" , "Icing Cutoff Temperature" , "C" ,"" , "Losses" , "en_icing_cutoff=1" , "" , "" } , { SSC_INPUT , SSC_NUMBER , "icing_cutoff_rh" , "Icing Cutoff Relative Humidity" , "%" ,"'rh' required in wind_resource_data" , "Losses" , "en_icing_cutoff=1" , "MIN=0" , "" } , + { SSC_INPUT , SSC_NUMBER , "wake_loss_multiplier" , "Multiplier for the calculated wake loss" , "" ,">1 increases loss, <1 decreases loss", "Farm" , "" , "MIN=0" , "" } , + { SSC_INPUT , SSC_NUMBER , "wake_int_loss" , "Constant Wake Model, internal wake loss" , "%" ,"" , "Losses" , "wind_farm_wake_model=3" , "MIN=0,MAX=100" , "" } , { SSC_INPUT , SSC_NUMBER , "wake_ext_loss" , "External Wake loss" , "%" ,"" , "Losses" , "?=0" , "MIN=0,MAX=100" , "" } , { SSC_INPUT , SSC_NUMBER , "wake_future_loss" , "Future Wake loss" , "%" ,"" , "Losses" , "?=0" , "MIN=0,MAX=100" , "" } , @@ -362,6 +364,7 @@ void cm_windpower::exec() ssc_number_t gross_energy = turbine_kw * wpc.nTurbines; annual_wake_int_loss_percent = is_assigned("wake_int_loss") ? as_double("wake_int_loss") : 0.; + annual_wake_int_loss_percent *= is_assigned("wake_loss_multiplier") ? as_double("wake_loss_multiplier") : 1.; //to do- update this if input type changes turbine_kw = turbine_kw * lossMultiplier * (1. - annual_wake_int_loss_percent/100.); int nstep = 8760; @@ -424,6 +427,11 @@ void cm_windpower::exec() throw exec_error("windpower", wpc.GetErrorDetails()); } + if (is_assigned("wake_loss_multiplier")) + { + //to do- depends on type of input + } + if (wakeModelChoice != CONSTANTVALUE) { assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); @@ -620,6 +628,11 @@ void cm_windpower::exec() &DistCross[0])) throw exec_error("windpower", util::format("error in wind calculation at time %d, details: %s", i, wpc.GetErrorDetails().c_str())); + if (is_assigned("wake_loss_multiplier")) + { + //to do- depends on type of input + } + //wake loss calculations need to happen before other losses are applied annual_gross += gross_farmp / (ssc_number_t)steps_per_hour; annual_after_wake_loss += farmp / (ssc_number_t)steps_per_hour; diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index 4dbcfcd39..9a37dd9b6 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -85,7 +85,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { EXPECT_NEAR(monthly_energy, 2.8218e6, e) << "Simple: December"; ssc_number_t wake_loss; - ssc_data_get_number(data, "wake_losses", &wake_loss); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &wake_loss); EXPECT_NEAR(wake_loss, 1.546, 1e-3) << "Simple: Wake loss"; @@ -102,7 +102,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { monthly_energy = ssc_data_get_array(data, "monthly_energy", nullptr)[11]; EXPECT_NEAR(monthly_energy, 2.7472e6, e) << "Wasp: Dec"; - ssc_data_get_number(data, "wake_losses", &wake_loss); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &wake_loss); EXPECT_NEAR(wake_loss, 4.148, 1e-3) << "Wasp: Wake loss"; // Eddy Viscosity Model @@ -118,7 +118,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { monthly_energy = ssc_data_get_array(data, "monthly_energy", nullptr)[11]; EXPECT_NEAR(monthly_energy, 2.6398e6, e) << "Eddy: Dec"; - ssc_data_get_number(data, "wake_losses", &wake_loss); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &wake_loss); EXPECT_NEAR(wake_loss, 7.895, 1e-3) << "Eddy: Wake loss"; // Constant Loss Model @@ -132,7 +132,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { ssc_data_get_number(data, "annual_gross_energy", &gross); EXPECT_NEAR(annual_energy, gross * 0.95, e) << "Constant"; - ssc_data_get_number(data, "wake_losses", &wake_loss); + ssc_data_get_number(data, "annual_total_wake_loss_percent", &wake_loss); //this wake model option doesn't report internal wake loss as an output EXPECT_NEAR(wake_loss, 5, 1e-3) << "Constant: Wake loss"; } From beab2821c836aa261730ed0faa64a1cf1ec63d62 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 14 Mar 2024 08:46:11 -0600 Subject: [PATCH 06/22] added optional wake decay constant for park wake model in wind model and updated test accordingly --- shared/lib_windwakemodel.cpp | 2 +- shared/lib_windwakemodel.h | 7 ++++--- ssc/cmod_windpower.cpp | 8 +++++++- test/shared_test/lib_windwakemodel_test.h | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/shared/lib_windwakemodel.cpp b/shared/lib_windwakemodel.cpp index 674070e97..8196d82e4 100644 --- a/shared/lib_windwakemodel.cpp +++ b/shared/lib_windwakemodel.cpp @@ -214,7 +214,7 @@ double parkWakeModel::delta_V_Park(double Uo, double Ui, double distCrosswind, d // bound the coeff of thrust double Ct = max_of(min_of(0.999, dThrustCoeff), minThrustCoeff); - double k = wakeDecayCoefficient; + double k = wakeDecayConstant; double dRadiusOfWake = dRadiusUpstream + (k * distDownwind); // radius of circle formed by wake from upwind rotor double dAreaOverlap = circle_overlap(distCrosswind, dRadiusDownstream, dRadiusOfWake); diff --git a/shared/lib_windwakemodel.h b/shared/lib_windwakemodel.h index a53ff494f..504066491 100644 --- a/shared/lib_windwakemodel.h +++ b/shared/lib_windwakemodel.h @@ -151,17 +151,18 @@ class simpleWakeModel : public wakeModelBase{ class parkWakeModel : public wakeModelBase{ private: double rotorDiameter; - double wakeDecayCoefficient = 0.07, + double wakeDecayConstant = 0.07, minThrustCoeff = 0.02; double delta_V_Park(double dVelFreeStream, double dVelUpwind, double dDistCrossWind, double dDistDownWind, double dRadiusUpstream, double dRadiusDownstream, double dThrustCoeff); double circle_overlap(double dist_center_to_center, double rad1, double rad2); public: parkWakeModel(){ nTurbines = 0; } - parkWakeModel(size_t numberOfTurbinesInFarm, windTurbine* wt){ nTurbines = numberOfTurbinesInFarm; wTurbine = wt; } + parkWakeModel(size_t numberOfTurbinesInFarm, windTurbine* wt, double wdc) { nTurbines = numberOfTurbinesInFarm; wTurbine = wt; setWakeDecayConstant(wdc); } virtual ~parkWakeModel() {}; std::string getModelName() override { return "Park"; } void setRotorDiameter(double d){ rotorDiameter = d; } + void setWakeDecayConstant(double w) { wakeDecayConstant = w; } void wakeCalculations( /*INPUTS*/ const double airDensity, // not used in this model @@ -302,4 +303,4 @@ class constantWakeModel : public wakeModelBase ) override; }; -#endif \ No newline at end of file +#endif diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index ed35289e3..222e322ad 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -59,6 +59,7 @@ static var_info _cm_vtab_windpower[] = { { SSC_INPUT , SSC_NUMBER , "wind_turbine_max_cp" , "Max Coefficient of Power" , "" ,"" , "Turbine" , "wind_resource_model_choice=1" , "MIN=0" , "" } , { SSC_INPUT , SSC_NUMBER , "wind_farm_wake_model" , "Wake Model [Simple, Park, EV, Constant]" , "0/1/2/3" ,"" , "Farm" , "*" , "INTEGER" , "" } , + { SSC_INPUT , SSC_NUMBER , "park_wake_decay_constant" , "Wake decay constant for Park model" , "0..1" ,"" , "Farm" , "" , "" , "" } , { SSC_INPUT , SSC_NUMBER , "wind_resource_turbulence_coeff" , "Turbulence coefficient" , "%" ,"" , "Farm" , "*" , "MIN=0" , "" } , { SSC_INPUT , SSC_NUMBER , "system_capacity" , "Nameplate capacity" , "kW" ,"" , "Farm" , "*" , "MIN=0" , "" } , { SSC_INPUT , SSC_ARRAY , "wind_farm_xCoordinates" , "Turbine X coordinates" , "m" ,"" , "Farm" , "*" , "" , "" } , @@ -402,7 +403,12 @@ void cm_windpower::exec() if (wakeModelChoice == SIMPLE) wakeModel = std::make_shared(simpleWakeModel(wpc.nTurbines, &wt)); else if (wakeModelChoice == PARK) - wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt)); + { + double wdc = 0.07; //this is the default value + if (is_assigned("park_wake_decay_constant")) + wdc = as_double("park_wake_decay_constant"); + wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt, wdc)); + } else if (wakeModelChoice == EDDYVISCOSITY) { wpc.turbulenceIntensity *= 100; diff --git a/test/shared_test/lib_windwakemodel_test.h b/test/shared_test/lib_windwakemodel_test.h index e50813cb9..761fa8c7d 100644 --- a/test/shared_test/lib_windwakemodel_test.h +++ b/test/shared_test/lib_windwakemodel_test.h @@ -154,7 +154,7 @@ class parkWakeModelTest : public ::testing::Test{ windSpeed.resize(numberTurbines); turbIntensity.resize(numberTurbines, 0.1); createDefaultTurbine(&wt); - pm = parkWakeModel(numberTurbines, &wt); + pm = parkWakeModel(numberTurbines, &wt, 0.07); for (int i = 0; i < numberTurbines; i++){ windSpeed[i] = 10.; } From e0abe346dcaadd0011c2bbf492c370b4293a9acc Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 14 Mar 2024 10:10:23 -0600 Subject: [PATCH 07/22] added optional wake loss multiplier input to wind model --- ssc/cmod_windpower.cpp | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 327c17d7f..971929b11 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -365,7 +365,7 @@ void cm_windpower::exec() ssc_number_t gross_energy = turbine_kw * wpc.nTurbines; annual_wake_int_loss_percent = is_assigned("wake_int_loss") ? as_double("wake_int_loss") : 0.; - annual_wake_int_loss_percent *= is_assigned("wake_loss_multiplier") ? as_double("wake_loss_multiplier") : 1.; //to do- update this if input type changes + annual_wake_int_loss_percent *= is_assigned("wake_loss_multiplier") ? as_double("wake_loss_multiplier") : 1.; turbine_kw = turbine_kw * lossMultiplier * (1. - annual_wake_int_loss_percent/100.); int nstep = 8760; @@ -417,6 +417,7 @@ void cm_windpower::exec() else if (wakeModelChoice == CONSTANTVALUE) { annual_wake_int_loss_percent = as_double("wake_int_loss"); + annual_wake_int_loss_percent *= is_assigned("wake_loss_multiplier") ? as_double("wake_loss_multiplier") : 1.; // add any wake loss multipliers to the constant loss value here wakeModel = std::make_shared(constantWakeModel(wpc.nTurbines, &wt, (100. - annual_wake_int_loss_percent)/100.)); } else{ @@ -433,13 +434,15 @@ void cm_windpower::exec() throw exec_error("windpower", wpc.GetErrorDetails()); } - if (is_assigned("wake_loss_multiplier")) - { - //to do- depends on type of input - } - if (wakeModelChoice != CONSTANTVALUE) { + if (is_assigned("wake_loss_multiplier")) //wake loss multiplier is assigned for constant value wake option above in the wake model setup + { + double wakeLossMultiplier = as_double("wake_loss_multiplier"); + double wakeLossBeforeMultiplier = farmPowerGross - farmPower; + double newWakeLoss = wakeLossBeforeMultiplier * wakeLossMultiplier; + farmPower = farmPowerGross - newWakeLoss; + } assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); annual_wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; assign("annual_internal_wake_loss_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); @@ -634,9 +637,12 @@ void cm_windpower::exec() &DistCross[0])) throw exec_error("windpower", util::format("error in wind calculation at time %d, details: %s", i, wpc.GetErrorDetails().c_str())); - if (is_assigned("wake_loss_multiplier")) + if (wakeModelChoice != CONSTANTVALUE && is_assigned("wake_loss_multiplier")) //wake loss multiplier is assigned for constant value wake option above in the wake model setup { - //to do- depends on type of input + double wakeLossMultiplier = as_double("wake_loss_multiplier"); + double wakeLossBeforeMultiplier = gross_farmp - farmp; + double newWakeLoss = wakeLossBeforeMultiplier * wakeLossMultiplier; + farmp = gross_farmp - newWakeLoss; } //wake loss calculations need to happen before other losses are applied @@ -693,7 +699,7 @@ void cm_windpower::exec() wsp_avg /= nstep; assign("wind_speed_average", wsp_avg); - // internal wake loss is calculated during simulation rather than provided + // if internal wake loss is calculated during simulation rather than provided, assign these outputs if (wakeModelChoice != CONSTANTVALUE){ assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(annual_gross - annual_after_wake_loss))); annual_wake_int_loss_percent = (1. - annual_after_wake_loss/annual_gross) * 100.; From fc8c3103e4fcde113c4228120b0fd9b6754f455c Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 14 Mar 2024 10:37:56 -0600 Subject: [PATCH 08/22] added a new wind test for the wake model loss multiplier --- test/ssc_test/cmod_windpower_test.cpp | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index 9a37dd9b6..e933711b2 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -136,6 +136,58 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { EXPECT_NEAR(wake_loss, 5, 1e-3) << "Constant: Wake loss"; } +TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) +{ + ssc_number_t withoutMultiplier, withMultiplier; + ssc_number_t multiplier = 1.2; + + //Simple Wake Model + ssc_data_set_number(data, "wake_loss_multiplier", 1.0); + compute(); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withoutMultiplier); + ssc_data_set_number(data, "wake_loss_multiplier", multiplier); + compute(); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withMultiplier); + if (withoutMultiplier != 0.) + EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "Simple Wake Model Multiplier"; + + //WASP model (Park) + ssc_data_set_number(data, "wind_farm_wake_model", 1); + ssc_data_set_number(data, "wake_loss_multiplier", 1.0); + compute(); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withoutMultiplier); + ssc_data_set_number(data, "wake_loss_multiplier", multiplier); + compute(); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withMultiplier); + if (withoutMultiplier != 0.) + EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "WASP Wake Model Multiplier"; + + //Eddy Viscosity model + ssc_data_set_number(data, "wind_farm_wake_model", 2); + ssc_data_set_number(data, "wake_loss_multiplier", 1.0); + compute(); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withoutMultiplier); + ssc_data_set_number(data, "wake_loss_multiplier", multiplier); + compute(); + ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withMultiplier); + if (withoutMultiplier != 0.) + EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "EV Wake Model Multiplier"; + + //Constant Loss model + ssc_data_set_number(data, "wind_farm_wake_model", 3); + ssc_data_set_number(data, "wake_int_loss", 5); + ssc_data_set_number(data, "wake_loss_multiplier", 1.0); + compute(); + ssc_data_get_number(data, "annual_total_wake_loss_percent", &withoutMultiplier); + ssc_data_set_number(data, "wake_loss_multiplier", multiplier); + compute(); + ssc_data_get_number(data, "annual_total_wake_loss_percent", &withMultiplier); + if (withoutMultiplier != 0.) + EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "Constant Loss Wake Model Multiplier"; + + +} + /// Using Interpolated Subhourly Wind Data TEST_F(CMWindPowerIntegration, UsingInterpolatedSubhourly_cmod_windpower) { // Using AR Northwestern-Flat Lands From a0958b18255b942cf39ebe3612ab49f0d1b6a530 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 28 Mar 2024 11:04:14 -0600 Subject: [PATCH 09/22] added optional coefficient of thrust curve input to cmod_windpower for the park wake model. placeholder for functionality of using the input needs to be updated. --- shared/lib_windwakemodel.cpp | 7 +++++++ shared/lib_windwakemodel.h | 6 +++++- ssc/cmod_windpower.cpp | 22 ++++++++++++++++++++-- test/shared_test/lib_windwakemodel_test.h | 2 +- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/shared/lib_windwakemodel.cpp b/shared/lib_windwakemodel.cpp index 8196d82e4..4caa94186 100644 --- a/shared/lib_windwakemodel.cpp +++ b/shared/lib_windwakemodel.cpp @@ -214,6 +214,13 @@ double parkWakeModel::delta_V_Park(double Uo, double Ui, double distCrosswind, d // bound the coeff of thrust double Ct = max_of(min_of(0.999, dThrustCoeff), minThrustCoeff); + // overwrite the coefficient of thrust if it has been specified by the user + // if it has not been specified by the user, the thrust curve vector is {0.} + if (ctCurve.size() != 1) + { + // do something here + } + double k = wakeDecayConstant; double dRadiusOfWake = dRadiusUpstream + (k * distDownwind); // radius of circle formed by wake from upwind rotor diff --git a/shared/lib_windwakemodel.h b/shared/lib_windwakemodel.h index 504066491..c769ce27d 100644 --- a/shared/lib_windwakemodel.h +++ b/shared/lib_windwakemodel.h @@ -153,12 +153,16 @@ class parkWakeModel : public wakeModelBase{ double rotorDiameter; double wakeDecayConstant = 0.07, minThrustCoeff = 0.02; + std::vector ctCurve; //vector that stores the optional coefficient of thrust curve input double delta_V_Park(double dVelFreeStream, double dVelUpwind, double dDistCrossWind, double dDistDownWind, double dRadiusUpstream, double dRadiusDownstream, double dThrustCoeff); double circle_overlap(double dist_center_to_center, double rad1, double rad2); public: parkWakeModel(){ nTurbines = 0; } - parkWakeModel(size_t numberOfTurbinesInFarm, windTurbine* wt, double wdc) { nTurbines = numberOfTurbinesInFarm; wTurbine = wt; setWakeDecayConstant(wdc); } + parkWakeModel(size_t numberOfTurbinesInFarm, windTurbine* wt, double wdc, std::vector ctc) + { + nTurbines = numberOfTurbinesInFarm; wTurbine = wt; setWakeDecayConstant(wdc); ctCurve = ctc; + } virtual ~parkWakeModel() {}; std::string getModelName() override { return "Park"; } void setRotorDiameter(double d){ rotorDiameter = d; } diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 971929b11..67c6accb9 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -72,7 +72,9 @@ static var_info _cm_vtab_windpower[] = { { SSC_INPUT , SSC_NUMBER , "icing_cutoff_temp" , "Icing Cutoff Temperature" , "C" ,"" , "Losses" , "en_icing_cutoff=1" , "" , "" } , { SSC_INPUT , SSC_NUMBER , "icing_cutoff_rh" , "Icing Cutoff Relative Humidity" , "%" ,"'rh' required in wind_resource_data" , "Losses" , "en_icing_cutoff=1" , "MIN=0" , "" } , + // optional SDK only wake loss inputs { SSC_INPUT , SSC_NUMBER , "wake_loss_multiplier" , "Multiplier for the calculated wake loss" , "" ,">1 increases loss, <1 decreases loss", "Farm" , "" , "MIN=0" , "" } , + { SSC_INOUT , SSC_ARRAY , "wind_turbine_ct_curve" , "Park model coeff of thrust curve vs WS" , "" ,"uses same wind speeds as power curve", "Turbine" , "" , "LENGTH_EQUAL=wind_turbine_powercurve_windspeeds" , "GROUP=WTPCD" } , { SSC_INPUT , SSC_NUMBER , "wake_int_loss" , "Constant Wake Model, internal wake loss" , "%" ,"" , "Losses" , "wind_farm_wake_model=3" , "MIN=0,MAX=100" , "" } , { SSC_INPUT , SSC_NUMBER , "wake_ext_loss" , "External Wake loss" , "%" ,"" , "Losses" , "?=0" , "MIN=0,MAX=100" , "" } , @@ -291,7 +293,7 @@ void cm_windpower::exec() wt.rotorDiameter = as_double("wind_turbine_rotor_diameter"); ssc_number_t *pc_w = as_array("wind_turbine_powercurve_windspeeds", &wt.powerCurveArrayLength); if (wt.powerCurveArrayLength == 1) - throw exec_error("windpower", util::format("The wind turbine power curves has insufficient data. Consider changing the turbine design parameters")); + throw exec_error("windpower", util::format("The wind turbine power curve has insufficient data. Consider changing the turbine design parameters")); ssc_number_t *pc_p = as_array("wind_turbine_powercurve_powerout", NULL); std::vector windSpeeds(wt.powerCurveArrayLength), powerOutput(wt.powerCurveArrayLength); for (size_t i = 0; i < wt.powerCurveArrayLength; i++){ @@ -404,10 +406,26 @@ void cm_windpower::exec() wakeModel = std::make_shared(simpleWakeModel(wpc.nTurbines, &wt)); else if (wakeModelChoice == PARK) { + // get optional thrust curve + std::vector ct_curve = { 0. }; //initialize this to length 1: a power curve length of 1 throws an exec error above & the length of ct curve is tied to the power curve, so this is a safe null value + if (is_assigned("wind_turbine_ct_curve")) + { + size_t *ctCurveLength = 0; + ssc_number_t *ctc = as_array("wind_turbine_ct_curve", ctCurveLength); + if (*ctCurveLength != wt.powerCurveArrayLength) + throw exec_error("windpower", "Thrust curve must have same number of values as the power curve"); + ct_curve.resize(*ctCurveLength); + for (size_t i = 0; i < *ctCurveLength; i++) + ct_curve[i] = ctc[i]; + } + + // get optional wake decay constant double wdc = 0.07; //this is the default value if (is_assigned("park_wake_decay_constant")) wdc = as_double("park_wake_decay_constant"); - wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt, wdc)); + + // create the wake model + wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt, wdc, ct_curve)); } else if (wakeModelChoice == EDDYVISCOSITY) { diff --git a/test/shared_test/lib_windwakemodel_test.h b/test/shared_test/lib_windwakemodel_test.h index 761fa8c7d..559af4470 100644 --- a/test/shared_test/lib_windwakemodel_test.h +++ b/test/shared_test/lib_windwakemodel_test.h @@ -154,7 +154,7 @@ class parkWakeModelTest : public ::testing::Test{ windSpeed.resize(numberTurbines); turbIntensity.resize(numberTurbines, 0.1); createDefaultTurbine(&wt); - pm = parkWakeModel(numberTurbines, &wt, 0.07); + pm = parkWakeModel(numberTurbines, &wt, 0.07, { 0. }); //use default values for the last two inputs for (int i = 0; i < numberTurbines; i++){ windSpeed[i] = 10.; } From 1aeaa7f3b8640c91c68672484f53fa38ecaee3a3 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Fri, 29 Mar 2024 09:49:01 -0600 Subject: [PATCH 10/22] change implementation of optional user-defined thrust coefficient curve to belong to wind turbine class so that it will be available to all wake models --- shared/lib_windwakemodel.cpp | 27 ++++++++++++++++------ shared/lib_windwakemodel.h | 19 +++++++++------ ssc/cmod_windpower.cpp | 28 +++++++++++------------ test/shared_test/lib_windwakemodel_test.h | 2 +- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/shared/lib_windwakemodel.cpp b/shared/lib_windwakemodel.cpp index 4caa94186..6731ea22a 100644 --- a/shared/lib_windwakemodel.cpp +++ b/shared/lib_windwakemodel.cpp @@ -49,6 +49,18 @@ bool windTurbine::setPowerCurve(std::vector windSpeeds, std::vector thrustCoeffCurve) +{ + if (thrustCoeffCurve.size() != powerCurveWS.size()) + { + errDetails = "Coefficient of thrust curve must have the same number of values as the power curve wind speeds"; + return 0; + } + ctCurve.resize(thrustCoeffCurve.size()); + ctCurve = thrustCoeffCurve; + return 1; +} + double windTurbine::tipSpeedRatio(double windSpeed) { if (powerCurveRPM[0] == -1) return 7.0; @@ -132,6 +144,14 @@ void windTurbine::turbinePower(double windVelocity, double airDensity, double *t *turbineOutput = out_pwr; if (fPowerCoefficient >= 0.0) *thrustCoefficient = max_of(0.0, -1.453989e-2 + 1.473506*fPowerCoefficient - 2.330823*pow(fPowerCoefficient, 2) + 3.885123*pow(fPowerCoefficient, 3)); + + // overwrite the coefficient of thrust if it has been specified by the user + // if it has not been specified by the user, the thrust curve vector is {0.} + if (ctCurve.size() != 1) + { + // do something here + } + } // out_pwr > (rated power * 0.001) return; @@ -214,13 +234,6 @@ double parkWakeModel::delta_V_Park(double Uo, double Ui, double distCrosswind, d // bound the coeff of thrust double Ct = max_of(min_of(0.999, dThrustCoeff), minThrustCoeff); - // overwrite the coefficient of thrust if it has been specified by the user - // if it has not been specified by the user, the thrust curve vector is {0.} - if (ctCurve.size() != 1) - { - // do something here - } - double k = wakeDecayConstant; double dRadiusOfWake = dRadiusUpstream + (k * distDownwind); // radius of circle formed by wake from upwind rotor diff --git a/shared/lib_windwakemodel.h b/shared/lib_windwakemodel.h index c769ce27d..eeb459bd1 100644 --- a/shared/lib_windwakemodel.h +++ b/shared/lib_windwakemodel.h @@ -47,10 +47,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. class windTurbine { private: - std::vector powerCurveWS, // windspeed: x-axis on turbine power curve - powerCurveKW, // power output: y-axis - densityCorrectedWS, - powerCurveRPM; + std::vector powerCurveWS, // windspeed: x-axis on turbine power curve + powerCurveKW, // power output: y-axis + densityCorrectedWS, + powerCurveRPM; + // vector that stores the optional coefficient of thrust curve input + // set to a default value of length 1 to mean that it's not assigned, and check for that length before using it + std::vector ctCurve = { 0. }; double cutInSpeed; double previousAirDensity; public: @@ -70,6 +73,7 @@ class windTurbine previousAirDensity = physics::AIR_DENSITY_SEA_LEVEL; } bool setPowerCurve(std::vector windSpeeds, std::vector powerOutput); + bool setCtCurve(std::vector thrustCoeffCurve); double tipSpeedRatio(double windSpeed); @@ -153,15 +157,16 @@ class parkWakeModel : public wakeModelBase{ double rotorDiameter; double wakeDecayConstant = 0.07, minThrustCoeff = 0.02; - std::vector ctCurve; //vector that stores the optional coefficient of thrust curve input double delta_V_Park(double dVelFreeStream, double dVelUpwind, double dDistCrossWind, double dDistDownWind, double dRadiusUpstream, double dRadiusDownstream, double dThrustCoeff); double circle_overlap(double dist_center_to_center, double rad1, double rad2); public: parkWakeModel(){ nTurbines = 0; } - parkWakeModel(size_t numberOfTurbinesInFarm, windTurbine* wt, double wdc, std::vector ctc) + parkWakeModel(size_t numberOfTurbinesInFarm, windTurbine* wt, double wdc) { - nTurbines = numberOfTurbinesInFarm; wTurbine = wt; setWakeDecayConstant(wdc); ctCurve = ctc; + nTurbines = numberOfTurbinesInFarm; + wTurbine = wt; + setWakeDecayConstant(wdc); } virtual ~parkWakeModel() {}; std::string getModelName() override { return "Park"; } diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 67c6accb9..841fee6f3 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -74,7 +74,7 @@ static var_info _cm_vtab_windpower[] = { // optional SDK only wake loss inputs { SSC_INPUT , SSC_NUMBER , "wake_loss_multiplier" , "Multiplier for the calculated wake loss" , "" ,">1 increases loss, <1 decreases loss", "Farm" , "" , "MIN=0" , "" } , - { SSC_INOUT , SSC_ARRAY , "wind_turbine_ct_curve" , "Park model coeff of thrust curve vs WS" , "" ,"uses same wind speeds as power curve", "Turbine" , "" , "LENGTH_EQUAL=wind_turbine_powercurve_windspeeds" , "GROUP=WTPCD" } , + { SSC_INOUT , SSC_ARRAY , "wind_turbine_ct_curve" , "User-defined Ct curve vs WS for wake models", "" ,"uses same wind speeds as power curve", "Turbine" , "" , "LENGTH_EQUAL=wind_turbine_powercurve_windspeeds" , "GROUP=WTPCD" } , { SSC_INPUT , SSC_NUMBER , "wake_int_loss" , "Constant Wake Model, internal wake loss" , "%" ,"" , "Losses" , "wind_farm_wake_model=3" , "MIN=0,MAX=100" , "" } , { SSC_INPUT , SSC_NUMBER , "wake_ext_loss" , "External Wake loss" , "%" ,"" , "Losses" , "?=0" , "MIN=0,MAX=100" , "" } , @@ -302,6 +302,17 @@ void cm_windpower::exec() } wt.setPowerCurve(windSpeeds, powerOutput); + // get optional thrust curve for wind turbine + if (is_assigned("wind_turbine_ct_curve")) + { + size_t* ctCurveLength = 0; + ssc_number_t* ctc = as_array("wind_turbine_ct_curve", ctCurveLength); + std::vector ct_curve(*ctCurveLength); + for (size_t i = 0; i < *ctCurveLength; i++) + ct_curve[i] = ctc[i]; + wt.setCtCurve(ct_curve); + } + // create windPowerCalculator using windTurbine windPowerCalculator wpc; wpc.windTurb = &wt; @@ -406,26 +417,13 @@ void cm_windpower::exec() wakeModel = std::make_shared(simpleWakeModel(wpc.nTurbines, &wt)); else if (wakeModelChoice == PARK) { - // get optional thrust curve - std::vector ct_curve = { 0. }; //initialize this to length 1: a power curve length of 1 throws an exec error above & the length of ct curve is tied to the power curve, so this is a safe null value - if (is_assigned("wind_turbine_ct_curve")) - { - size_t *ctCurveLength = 0; - ssc_number_t *ctc = as_array("wind_turbine_ct_curve", ctCurveLength); - if (*ctCurveLength != wt.powerCurveArrayLength) - throw exec_error("windpower", "Thrust curve must have same number of values as the power curve"); - ct_curve.resize(*ctCurveLength); - for (size_t i = 0; i < *ctCurveLength; i++) - ct_curve[i] = ctc[i]; - } - // get optional wake decay constant double wdc = 0.07; //this is the default value if (is_assigned("park_wake_decay_constant")) wdc = as_double("park_wake_decay_constant"); // create the wake model - wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt, wdc, ct_curve)); + wakeModel = std::make_shared(parkWakeModel(wpc.nTurbines, &wt, wdc)); } else if (wakeModelChoice == EDDYVISCOSITY) { diff --git a/test/shared_test/lib_windwakemodel_test.h b/test/shared_test/lib_windwakemodel_test.h index 559af4470..49a9f7cc1 100644 --- a/test/shared_test/lib_windwakemodel_test.h +++ b/test/shared_test/lib_windwakemodel_test.h @@ -154,7 +154,7 @@ class parkWakeModelTest : public ::testing::Test{ windSpeed.resize(numberTurbines); turbIntensity.resize(numberTurbines, 0.1); createDefaultTurbine(&wt); - pm = parkWakeModel(numberTurbines, &wt, 0.07, { 0. }); //use default values for the last two inputs + pm = parkWakeModel(numberTurbines, &wt, 0.07); for (int i = 0; i < numberTurbines; i++){ windSpeed[i] = 10.; } From 93b5771c0e896fe6f11471600a281a35bd20a724 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Fri, 29 Mar 2024 11:07:21 -0600 Subject: [PATCH 11/22] added functionality to overwrite calculated coefficient of thrust with a user-defined Ct curve for all wind wake models --- shared/lib_windwakemodel.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/shared/lib_windwakemodel.cpp b/shared/lib_windwakemodel.cpp index 6731ea22a..17850ad97 100644 --- a/shared/lib_windwakemodel.cpp +++ b/shared/lib_windwakemodel.cpp @@ -116,9 +116,9 @@ void windTurbine::turbinePower(double windVelocity, double airDensity, double *t // Find power from turbine power curve double out_pwr = 0.0; + int j = 1; // an index for the correct wind speed in the power curve for interpolations if ((windVelocity > densityCorrectedWS[0]) && (windVelocity < densityCorrectedWS[powerCurveArrayLength - 1])) { - int j = 1; while (densityCorrectedWS[j] <= windVelocity) j++; // find first m_adPowerCurveWS > windVelocity @@ -149,7 +149,9 @@ void windTurbine::turbinePower(double windVelocity, double airDensity, double *t // if it has not been specified by the user, the thrust curve vector is {0.} if (ctCurve.size() != 1) { - // do something here + //interpolate to right value of Ct based on wind speed, like for power curve + //can use the same index for the wind speed array that was determined above + *thrustCoefficient = util::interpolate(densityCorrectedWS[j - 1], ctCurve[j - 1], densityCorrectedWS[j], ctCurve[j], windVelocity); } } // out_pwr > (rated power * 0.001) From 762f4899041a511fe5d8530564d2337100b1b05e Mon Sep 17 00:00:00 2001 From: janinefreeman Date: Wed, 3 Apr 2024 11:33:45 -0600 Subject: [PATCH 12/22] modified where coefficient of thrust is initialized --- shared/lib_windwakemodel.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shared/lib_windwakemodel.h b/shared/lib_windwakemodel.h index eeb459bd1..215bc3ba6 100644 --- a/shared/lib_windwakemodel.h +++ b/shared/lib_windwakemodel.h @@ -53,7 +53,7 @@ class windTurbine powerCurveRPM; // vector that stores the optional coefficient of thrust curve input // set to a default value of length 1 to mean that it's not assigned, and check for that length before using it - std::vector ctCurve = { 0. }; + std::vector ctCurve; double cutInSpeed; double previousAirDensity; public: @@ -71,6 +71,7 @@ class windTurbine hubHeight = -999; rotorDiameter = -999; previousAirDensity = physics::AIR_DENSITY_SEA_LEVEL; + ctCurve = { 0.0 }; } bool setPowerCurve(std::vector windSpeeds, std::vector powerOutput); bool setCtCurve(std::vector thrustCoeffCurve); From 419458a4f941cc76f4034d1ebe2754b92553b84b Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 4 Apr 2024 10:31:35 -0600 Subject: [PATCH 13/22] updated ctcurve initialization --- shared/lib_windwakemodel.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/shared/lib_windwakemodel.h b/shared/lib_windwakemodel.h index 215bc3ba6..365eee408 100644 --- a/shared/lib_windwakemodel.h +++ b/shared/lib_windwakemodel.h @@ -50,10 +50,8 @@ class windTurbine std::vector powerCurveWS, // windspeed: x-axis on turbine power curve powerCurveKW, // power output: y-axis densityCorrectedWS, - powerCurveRPM; - // vector that stores the optional coefficient of thrust curve input - // set to a default value of length 1 to mean that it's not assigned, and check for that length before using it - std::vector ctCurve; + powerCurveRPM, + ctCurve; // vector that stores the optional coefficient of thrust curve input double cutInSpeed; double previousAirDensity; public: @@ -71,7 +69,7 @@ class windTurbine hubHeight = -999; rotorDiameter = -999; previousAirDensity = physics::AIR_DENSITY_SEA_LEVEL; - ctCurve = { 0.0 }; + ctCurve = { 0.0 }; // set to a default value of length 1 to mean that it's not assigned, and check for that length before using it } bool setPowerCurve(std::vector windSpeeds, std::vector powerOutput); bool setCtCurve(std::vector thrustCoeffCurve); From a3393f578280c7038cbb9302a8016d2c4197e1ca Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 4 Apr 2024 11:04:50 -0600 Subject: [PATCH 14/22] add check for power curve existence in thrust curve set function and update wake loss output names per Paul suggestions --- shared/lib_windwakemodel.cpp | 5 +++++ ssc/cmod_windpower.cpp | 24 ++++++++++++------------ test/ssc_test/cmod_windpower_test.cpp | 24 ++++++++++++------------ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/shared/lib_windwakemodel.cpp b/shared/lib_windwakemodel.cpp index 17850ad97..c835db2d3 100644 --- a/shared/lib_windwakemodel.cpp +++ b/shared/lib_windwakemodel.cpp @@ -51,6 +51,11 @@ bool windTurbine::setPowerCurve(std::vector windSpeeds, std::vector thrustCoeffCurve) { + if (powerCurveWS.empty()) + { + errDetails = "Power curve must be set before thrust curve."; + return 0; + } if (thrustCoeffCurve.size() != powerCurveWS.size()) { errDetails = "Coefficient of thrust curve must have the same number of values as the power curve wind speeds"; diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 841fee6f3..5b609197b 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -114,8 +114,8 @@ static var_info _cm_vtab_windpower[] = { { SSC_OUTPUT , SSC_NUMBER , "year" , "Year" , "" ,"" , "Location" , "wind_resource_model_choice=0" , "" , "" } , // timeseries outputs - { SSC_OUTPUT , SSC_ARRAY , "timeseries_wake_loss_kW" , "Wake loss at each timestep in kW" , "kW" ,"" , "Time Series" , "" , "" , "" } , - { SSC_OUTPUT , SSC_ARRAY , "timeseries_wake_loss_percent" , "Wake loss at each timestep percent" , "%" ,"" , "Time Series" , "" , "" , "" } , + { SSC_OUTPUT , SSC_ARRAY , "wake_loss_internal_kW" , "Internal wake loss in kW" , "kW" ,"" , "Time Series" , "" , "" , "" } , + { SSC_OUTPUT , SSC_ARRAY , "wake_loss_internal_percent" , "Internal wake loss percent" , "%" ,"" , "Time Series" , "" , "" , "" } , // monthly and annual outputs { SSC_OUTPUT , SSC_ARRAY , "monthly_energy" , "Monthly Energy Gross" , "kWh" ,"" , "Monthly" , "*" , "LENGTH=12" , "" } , @@ -131,9 +131,9 @@ static var_info _cm_vtab_windpower[] = { { SSC_OUTPUT , SSC_NUMBER , "env_losses" , "Environmental losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "ops_losses" , "Operational losses" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "turb_losses" , "Turbine losses" , "%" ,"" , "Annual" ,"" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "annual_internal_wake_loss_percent" , "Annual internal wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "annual_internal_wake_loss_kWh" , "Annual internal wake loss" , "kWh" ,"" , "Annual" ,"" , "" , "" } , - { SSC_OUTPUT , SSC_NUMBER , "annual_total_wake_loss_percent" , "Annual total wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_internal_percent" , "Annual internal wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_internal_kWh" , "Annual internal wake loss" , "kWh" ,"" , "Annual" ,"" , "" , "" } , + { SSC_OUTPUT , SSC_NUMBER , "annual_wake_loss_total_percent" , "Annual total wake loss percentage" , "%" ,"" , "Annual" ,"" , "" , "" } , { SSC_OUTPUT , SSC_NUMBER , "cutoff_losses" , "Low temp and Icing Cutoff losses" , "%" ,"" , "Annual" ,"" , "" , "" } , var_info_invalid }; @@ -266,7 +266,7 @@ void calculate_losses(compute_module *cm, double annual_wake_int_loss_percent) { cm->assign("env_losses", env_loss_percent * 100.); cm->assign("ops_losses", ops_loss_percent * 100.); cm->assign("turb_losses", turb_loss_percent * 100.); - cm->assign("annual_total_wake_loss_percent", total_wake_loss_percent * 100.); + cm->assign("annual_wake_loss_total_percent", total_wake_loss_percent * 100.); } double get_fixed_losses(compute_module* cm){ @@ -459,9 +459,9 @@ void cm_windpower::exec() double newWakeLoss = wakeLossBeforeMultiplier * wakeLossMultiplier; farmPower = farmPowerGross - newWakeLoss; } - assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); + assign("annual_wake_loss_internal_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); annual_wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; - assign("annual_internal_wake_loss_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); + assign("annual_wake_loss_internal_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); } int nstep = 8760; @@ -557,8 +557,8 @@ void cm_windpower::exec() // allocate output data ssc_number_t *farmpwr = allocate("gen", nstep); - ssc_number_t* wakeLosskW = allocate("timeseries_wake_loss_kW", nstep); - ssc_number_t* wakeLossPercent = allocate("timeseries_wake_loss_percent", nstep); + ssc_number_t* wakeLosskW = allocate("wake_loss_internal_kW", nstep); + ssc_number_t* wakeLossPercent = allocate("wake_loss_internal_percent", nstep); ssc_number_t *wspd = allocate("wind_speed", nstep); ssc_number_t *wdir = allocate("wind_direction", nstep); ssc_number_t *air_temp = allocate("temp", nstep); @@ -717,9 +717,9 @@ void cm_windpower::exec() // if internal wake loss is calculated during simulation rather than provided, assign these outputs if (wakeModelChoice != CONSTANTVALUE){ - assign("annual_internal_wake_loss_kWh", var_data((ssc_number_t)(annual_gross - annual_after_wake_loss))); + assign("annual_wake_loss_internal_kWh", var_data((ssc_number_t)(annual_gross - annual_after_wake_loss))); annual_wake_int_loss_percent = (1. - annual_after_wake_loss/annual_gross) * 100.; - assign("annual_internal_wake_loss_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); + assign("annual_wake_loss_internal_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); } calculate_p50p90(this); diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index e933711b2..9ab6845ca 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -85,7 +85,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { EXPECT_NEAR(monthly_energy, 2.8218e6, e) << "Simple: December"; ssc_number_t wake_loss; - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &wake_loss); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wake_loss); EXPECT_NEAR(wake_loss, 1.546, 1e-3) << "Simple: Wake loss"; @@ -102,7 +102,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { monthly_energy = ssc_data_get_array(data, "monthly_energy", nullptr)[11]; EXPECT_NEAR(monthly_energy, 2.7472e6, e) << "Wasp: Dec"; - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &wake_loss); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wake_loss); EXPECT_NEAR(wake_loss, 4.148, 1e-3) << "Wasp: Wake loss"; // Eddy Viscosity Model @@ -118,7 +118,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { monthly_energy = ssc_data_get_array(data, "monthly_energy", nullptr)[11]; EXPECT_NEAR(monthly_energy, 2.6398e6, e) << "Eddy: Dec"; - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &wake_loss); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wake_loss); EXPECT_NEAR(wake_loss, 7.895, 1e-3) << "Eddy: Wake loss"; // Constant Loss Model @@ -132,7 +132,7 @@ TEST_F(CMWindPowerIntegration, WakeModelsUsingFile_cmod_windpower) { ssc_data_get_number(data, "annual_gross_energy", &gross); EXPECT_NEAR(annual_energy, gross * 0.95, e) << "Constant"; - ssc_data_get_number(data, "annual_total_wake_loss_percent", &wake_loss); //this wake model option doesn't report internal wake loss as an output + ssc_data_get_number(data, "annual_wake_loss_total_percent", &wake_loss); //this wake model option doesn't report internal wake loss as an output EXPECT_NEAR(wake_loss, 5, 1e-3) << "Constant: Wake loss"; } @@ -144,10 +144,10 @@ TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) //Simple Wake Model ssc_data_set_number(data, "wake_loss_multiplier", 1.0); compute(); - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withoutMultiplier); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &withoutMultiplier); ssc_data_set_number(data, "wake_loss_multiplier", multiplier); compute(); - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withMultiplier); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &withMultiplier); if (withoutMultiplier != 0.) EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "Simple Wake Model Multiplier"; @@ -155,10 +155,10 @@ TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) ssc_data_set_number(data, "wind_farm_wake_model", 1); ssc_data_set_number(data, "wake_loss_multiplier", 1.0); compute(); - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withoutMultiplier); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &withoutMultiplier); ssc_data_set_number(data, "wake_loss_multiplier", multiplier); compute(); - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withMultiplier); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &withMultiplier); if (withoutMultiplier != 0.) EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "WASP Wake Model Multiplier"; @@ -166,10 +166,10 @@ TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) ssc_data_set_number(data, "wind_farm_wake_model", 2); ssc_data_set_number(data, "wake_loss_multiplier", 1.0); compute(); - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withoutMultiplier); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &withoutMultiplier); ssc_data_set_number(data, "wake_loss_multiplier", multiplier); compute(); - ssc_data_get_number(data, "annual_internal_wake_loss_percent", &withMultiplier); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &withMultiplier); if (withoutMultiplier != 0.) EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "EV Wake Model Multiplier"; @@ -178,10 +178,10 @@ TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) ssc_data_set_number(data, "wake_int_loss", 5); ssc_data_set_number(data, "wake_loss_multiplier", 1.0); compute(); - ssc_data_get_number(data, "annual_total_wake_loss_percent", &withoutMultiplier); + ssc_data_get_number(data, "annual_wake_loss_total_percent", &withoutMultiplier); ssc_data_set_number(data, "wake_loss_multiplier", multiplier); compute(); - ssc_data_get_number(data, "annual_total_wake_loss_percent", &withMultiplier); + ssc_data_get_number(data, "annual_wake_loss_total_percent", &withMultiplier); if (withoutMultiplier != 0.) EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "Constant Loss Wake Model Multiplier"; From b583981965364316f405fa96a080bd7f90f4b033 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 4 Apr 2024 11:29:37 -0600 Subject: [PATCH 15/22] added a test for the thrust curve input to wind compute module, still needs to be completed --- test/ssc_test/cmod_windpower_test.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index 9ab6845ca..398f2f3e7 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -188,6 +188,33 @@ TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) } +/// Test using the optional coefficient of thrust curve input for the park model +TEST_F(CMWindPowerIntegration, CtCurve_cmod_windpower) +{ + //set the park wake model and get original wake loss + ssc_data_set_number(data, "wind_farm_wake_model", 1); + compute(); + ssc_number_t wakeLoss1, wakeLoss2; + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wakeLoss1); //get the wake loss without setting Ct curve + + // use a bogus ct curve just to test that it does something + double ctc[161]; + for (int i = 0; i < 161; i++) + ctc[i] = 0.5; + var_data ct = var_data(ctc, 161, 1); + auto* vt = static_cast(data); + vt->assign("wind_turbine_ct_curve", ct); + + // get new wake loss + compute(); + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wakeLoss2); //get the wake loss without setting Ct curve + + ssc_number_t difference = wakeLoss2 - wakeLoss1; + EXPECT_NEAR(wakeLoss1, 4.148, 0.01) << "Wake Loss 1"; + EXPECT_NEAR(wakeLoss2, 10.0, 0.5) << "Wake Loss 2"; + EXPECT_NEAR(difference, 10.0, 0.5) << "Difference"; +} + /// Using Interpolated Subhourly Wind Data TEST_F(CMWindPowerIntegration, UsingInterpolatedSubhourly_cmod_windpower) { // Using AR Northwestern-Flat Lands From aa4f9102ad7510f6c5c99c838bc923fccf7ff29b Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Fri, 5 Apr 2024 11:47:05 -0600 Subject: [PATCH 16/22] updated wind test --- test/ssc_test/cmod_windpower_test.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index 398f2f3e7..1f82acf2b 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -198,16 +198,17 @@ TEST_F(CMWindPowerIntegration, CtCurve_cmod_windpower) ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wakeLoss1); //get the wake loss without setting Ct curve // use a bogus ct curve just to test that it does something - double ctc[161]; - for (int i = 0; i < 161; i++) - ctc[i] = 0.5; - var_data ct = var_data(ctc, 161, 1); - auto* vt = static_cast(data); - vt->assign("wind_turbine_ct_curve", ct); - + ssc_number_t ctCurve[161] = { 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, + 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5 }; + ssc_data_set_array(data, "wind_turbine_ct_curve", ctCurve, 161); + // get new wake loss compute(); - ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wakeLoss2); //get the wake loss without setting Ct curve + ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wakeLoss2); //get the wake loss with Ct curve ssc_number_t difference = wakeLoss2 - wakeLoss1; EXPECT_NEAR(wakeLoss1, 4.148, 0.01) << "Wake Loss 1"; From 4d778182860d9a4aa5df572385573fbfe487638a Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Thu, 11 Apr 2024 13:00:21 -0600 Subject: [PATCH 17/22] fixed variable ingestion of ct curve in wind cmod and updated thrust curve test --- ssc/cmod_windpower.cpp | 8 ++++---- test/ssc_test/cmod_windpower_test.cpp | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 5b609197b..9b5e9c215 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -305,10 +305,10 @@ void cm_windpower::exec() // get optional thrust curve for wind turbine if (is_assigned("wind_turbine_ct_curve")) { - size_t* ctCurveLength = 0; - ssc_number_t* ctc = as_array("wind_turbine_ct_curve", ctCurveLength); - std::vector ct_curve(*ctCurveLength); - for (size_t i = 0; i < *ctCurveLength; i++) + size_t ctCurveLength = 0; + ssc_number_t* ctc = as_array("wind_turbine_ct_curve", &ctCurveLength); + std::vector ct_curve(ctCurveLength); + for (size_t i = 0; i < ctCurveLength; i++) ct_curve[i] = ctc[i]; wt.setCtCurve(ct_curve); } diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index 1f82acf2b..76b1aeb23 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -212,8 +212,8 @@ TEST_F(CMWindPowerIntegration, CtCurve_cmod_windpower) ssc_number_t difference = wakeLoss2 - wakeLoss1; EXPECT_NEAR(wakeLoss1, 4.148, 0.01) << "Wake Loss 1"; - EXPECT_NEAR(wakeLoss2, 10.0, 0.5) << "Wake Loss 2"; - EXPECT_NEAR(difference, 10.0, 0.5) << "Difference"; + EXPECT_NEAR(wakeLoss2, 5.562, 0.5) << "Wake Loss 2"; + EXPECT_NEAR(difference, 1.413, 0.5) << "Difference"; } /// Using Interpolated Subhourly Wind Data From e1289c0ec3e63b296b65c8e143a4467c9987795b Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Sun, 21 Jul 2024 14:03:50 -0600 Subject: [PATCH 18/22] added icing loss persistence input to allow icing loss to last for more than just the timestep triggering the loss --- ssc/cmod_windpower.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 9b5e9c215..8850ed862 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -71,6 +71,7 @@ static var_info _cm_vtab_windpower[] = { { SSC_INPUT , SSC_NUMBER , "en_icing_cutoff" , "Enable Icing Cutoff" , "0/1" ,"" , "Losses" , "?=0" , "INTEGER" , "" } , { SSC_INPUT , SSC_NUMBER , "icing_cutoff_temp" , "Icing Cutoff Temperature" , "C" ,"" , "Losses" , "en_icing_cutoff=1" , "" , "" } , { SSC_INPUT , SSC_NUMBER , "icing_cutoff_rh" , "Icing Cutoff Relative Humidity" , "%" ,"'rh' required in wind_resource_data" , "Losses" , "en_icing_cutoff=1" , "MIN=0" , "" } , + { SSC_INPUT , SSC_NUMBER , "icing_persistence_timesteps" , "Num timesteps icing lasts if conditions are met","" ,"includes initial timestep" , "Losses" , "?=1" , "MIN=1,INTEGER" , "" } , // optional SDK only wake loss inputs { SSC_INPUT , SSC_NUMBER , "wake_loss_multiplier" , "Multiplier for the calculated wake loss" , "" ,">1 increases loss, <1 decreases loss", "Farm" , "" , "MIN=0" , "" } , @@ -358,6 +359,7 @@ void cm_windpower::exec() bool icingCutoff = as_boolean("en_icing_cutoff"); double icingTempCutoffValue = icingCutoff ? as_double("icing_cutoff_temp") : -1; double icingRHCutoffValue = icingCutoff ? as_double("icing_cutoff_rh") : -1; + int icingPersistenceTimesteps = as_integer("icing_persistence_timesteps"); // Run Weibull Statistical model (single outputs) if selected if (as_integer("wind_resource_model_choice") == 1 ){ @@ -576,6 +578,7 @@ void cm_windpower::exec() double annual_gross = 0.0; //annual energy before any losses in kWh double withoutCutOffLosses = 0.0; //annual energy without low temperature/icing cutoff losses applied in kWh double annual_after_wake_loss = 0.0; //annual energy after wake losses but before other losses in kWh + int icingPersistenceTSRemaining = 0; //a counter for icing to persist x number of timesteps based on user input // compute power output at i-th timestep int i = 0; @@ -675,8 +678,16 @@ void cm_windpower::exec() if (temp < lowTempCutoffValue) farmp = 0.0; } if (icingCutoff){ - if (temp < icingTempCutoffValue && wdprov->relativeHumidity()[i] > icingRHCutoffValue) - farmp = 0.0; + if (temp < icingTempCutoffValue && wdprov->relativeHumidity()[i] > icingRHCutoffValue) + { + farmp = 0.0; + icingPersistenceTSRemaining = icingPersistenceTimesteps; + } + if (icingPersistenceTSRemaining > 0) + { + farmp = 0.0; + icingPersistenceTSRemaining--; + } } farmpwr[i] = (ssc_number_t)farmp*haf(hr); //adjustment factors are constrained to be hourly, not sub-hourly, so it's correct for this to be indexed on the hour From 798cb1de8fa143156438f2e0a6f1348a10f27904 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Tue, 30 Jul 2024 16:53:20 -0600 Subject: [PATCH 19/22] added test for icing persistence --- test/ssc_test/cmod_windpower_test.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index c39c3e1ec..64f33af64 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -456,6 +456,12 @@ TEST_F(CMWindPowerIntegration, IcingAndLowTempCutoff_cmod_windpower) { ssc_data_get_number(data, "cutoff_losses", &losses_percent); EXPECT_NEAR(losses_percent, 0.5, 0.01); + //now test the persistence feature + vt->assign("icing_persistence_timesteps", 100); + compute(); + ssc_data_get_number(data, "cutoff_losses", &losses_percent); + EXPECT_NEAR(losses_percent, 1, 0.01); + free_winddata_array(windresourcedata); } From 26c584c3eed4aa16f8f9ef2ab3142b6b2c415474 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Fri, 4 Oct 2024 11:23:21 -0600 Subject: [PATCH 20/22] added error if ct curve setting fails --- ssc/cmod_windpower.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 8850ed862..9d0e68441 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -311,7 +311,12 @@ void cm_windpower::exec() std::vector ct_curve(ctCurveLength); for (size_t i = 0; i < ctCurveLength; i++) ct_curve[i] = ctc[i]; - wt.setCtCurve(ct_curve); + bool error = false; + error = wt.setCtCurve(ct_curve); + if (error) //function will return a zero if it fails + { + throw exec_error("windpower", wt.errDetails); + } } // create windPowerCalculator using windTurbine From 83a96ab966f71845988f9ad921220e51c87481e5 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Sat, 12 Oct 2024 14:36:25 -0600 Subject: [PATCH 21/22] updated wind model to disallow wake loss multiplier with constant wake model option and updated tests accordingly --- ssc/cmod_windpower.cpp | 26 +++++++++++++------------- test/ssc_test/cmod_windpower_test.cpp | 10 ++-------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 9d0e68441..612537d1e 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -311,9 +311,9 @@ void cm_windpower::exec() std::vector ct_curve(ctCurveLength); for (size_t i = 0; i < ctCurveLength; i++) ct_curve[i] = ctc[i]; - bool error = false; - error = wt.setCtCurve(ct_curve); - if (error) //function will return a zero if it fails + bool success = false; + success = wt.setCtCurve(ct_curve); + if (!success) //function will return a zero if it fails { throw exec_error("windpower", wt.errDetails); } @@ -385,6 +385,10 @@ void cm_windpower::exec() ssc_number_t gross_energy = turbine_kw * wpc.nTurbines; annual_wake_int_loss_percent = is_assigned("wake_int_loss") ? as_double("wake_int_loss") : 0.; + if (is_assigned("wake_int_loss") && is_assigned("wake_loss_multiplier")) + { + throw exec_error("windpower", "A wake loss multiplier may not be assigned with a constant wake loss value."); + } annual_wake_int_loss_percent *= is_assigned("wake_loss_multiplier") ? as_double("wake_loss_multiplier") : 1.; turbine_kw = turbine_kw * lossMultiplier * (1. - annual_wake_int_loss_percent/100.); @@ -440,7 +444,10 @@ void cm_windpower::exec() else if (wakeModelChoice == CONSTANTVALUE) { annual_wake_int_loss_percent = as_double("wake_int_loss"); - annual_wake_int_loss_percent *= is_assigned("wake_loss_multiplier") ? as_double("wake_loss_multiplier") : 1.; // add any wake loss multipliers to the constant loss value here + if (is_assigned("wake_loss_multiplier")) //wake loss multiplier is assigned for constant value wake option, should throw an error + { + throw exec_error("windpower", "A wake loss multiplier may not be assigned with a constant wake loss value."); + } wakeModel = std::make_shared(constantWakeModel(wpc.nTurbines, &wt, (100. - annual_wake_int_loss_percent)/100.)); } else{ @@ -457,15 +464,8 @@ void cm_windpower::exec() throw exec_error("windpower", wpc.GetErrorDetails()); } - if (wakeModelChoice != CONSTANTVALUE) + if (wakeModelChoice != CONSTANTVALUE) //we already threw an error if a wake loss multiplier was specified, so may just proceed { - if (is_assigned("wake_loss_multiplier")) //wake loss multiplier is assigned for constant value wake option above in the wake model setup - { - double wakeLossMultiplier = as_double("wake_loss_multiplier"); - double wakeLossBeforeMultiplier = farmPowerGross - farmPower; - double newWakeLoss = wakeLossBeforeMultiplier * wakeLossMultiplier; - farmPower = farmPowerGross - newWakeLoss; - } assign("annual_wake_loss_internal_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); annual_wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; assign("annual_wake_loss_internal_percent", var_data((ssc_number_t)annual_wake_int_loss_percent)); @@ -661,7 +661,7 @@ void cm_windpower::exec() &DistCross[0])) throw exec_error("windpower", util::format("error in wind calculation at time %d, details: %s", i, wpc.GetErrorDetails().c_str())); - if (wakeModelChoice != CONSTANTVALUE && is_assigned("wake_loss_multiplier")) //wake loss multiplier is assigned for constant value wake option above in the wake model setup + if (wakeModelChoice != CONSTANTVALUE && is_assigned("wake_loss_multiplier")) //wake loss multiplier isn't available for constant wake loss option { double wakeLossMultiplier = as_double("wake_loss_multiplier"); double wakeLossBeforeMultiplier = gross_farmp - farmp; diff --git a/test/ssc_test/cmod_windpower_test.cpp b/test/ssc_test/cmod_windpower_test.cpp index 64f33af64..3cc882ce4 100644 --- a/test/ssc_test/cmod_windpower_test.cpp +++ b/test/ssc_test/cmod_windpower_test.cpp @@ -177,13 +177,7 @@ TEST_F(CMWindPowerIntegration, WakeLossMultiplier_cmod_windpower) ssc_data_set_number(data, "wind_farm_wake_model", 3); ssc_data_set_number(data, "wake_int_loss", 5); ssc_data_set_number(data, "wake_loss_multiplier", 1.0); - compute(); - ssc_data_get_number(data, "annual_wake_loss_total_percent", &withoutMultiplier); - ssc_data_set_number(data, "wake_loss_multiplier", multiplier); - compute(); - ssc_data_get_number(data, "annual_wake_loss_total_percent", &withMultiplier); - if (withoutMultiplier != 0.) - EXPECT_NEAR((withMultiplier / withoutMultiplier), multiplier, 0.01) << "Constant Loss Wake Model Multiplier"; + EXPECT_FALSE( compute() ); //setting wake loss multiplier with constant loss value should return an error } @@ -207,7 +201,7 @@ TEST_F(CMWindPowerIntegration, CtCurve_cmod_windpower) ssc_data_set_array(data, "wind_turbine_ct_curve", ctCurve, 161); // get new wake loss - compute(); + EXPECT_TRUE(compute()); ssc_data_get_number(data, "annual_wake_loss_internal_percent", &wakeLoss2); //get the wake loss with Ct curve ssc_number_t difference = wakeLoss2 - wakeLoss1; From 6b70a2d95f09e8839abbda5cd71681e594006f71 Mon Sep 17 00:00:00 2001 From: Janine Keith Date: Tue, 15 Oct 2024 10:10:36 -0600 Subject: [PATCH 22/22] add wake loss multiplier code to wind speed x direction distribution mode --- ssc/cmod_windpower.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ssc/cmod_windpower.cpp b/ssc/cmod_windpower.cpp index 612537d1e..bb24bec6b 100644 --- a/ssc/cmod_windpower.cpp +++ b/ssc/cmod_windpower.cpp @@ -466,6 +466,13 @@ void cm_windpower::exec() if (wakeModelChoice != CONSTANTVALUE) //we already threw an error if a wake loss multiplier was specified, so may just proceed { + if (is_assigned("wake_loss_multiplier")) + { + double wakeLossMultiplier = as_double("wake_loss_multiplier"); + double wakeLossBeforeMultiplier = farmPowerGross - farmPower; + double newWakeLoss = wakeLossBeforeMultiplier * wakeLossMultiplier; + farmPower = farmPowerGross - newWakeLoss; + } assign("annual_wake_loss_internal_kWh", var_data((ssc_number_t)(farmPowerGross - farmPower))); annual_wake_int_loss_percent = (1. - farmPower / farmPowerGross) * 100.; assign("annual_wake_loss_internal_percent", var_data((ssc_number_t)annual_wake_int_loss_percent));