Optimiser cost benefit script
A Cost Benefit Algorithm Script Type is available to allow defining a custom target function for the Optimiser. This script type implements interface IScriptingCostBenefitAlgorithm which has a defined structure. When adding a new script of this type, a template is created with the necessary structure that we will need to adapt in order to meet our needs.
The template has a couple of rules that need to be followed and there are options that can be set. Looking at the class signature, the class name is user defined but a matching constructor needs to exist.
LargeValueIsBetter or SmallerValueIsBetter
The class needs to implement IScriptingCostBenefitAlgorithm and it also needs to derive from LargerValueIsBetterCostBenefitAlgorithmBase or SmallerValueIsBetterCostBenefitAlgorithmBase
- When the class derives from LargerValueIsBetterCostBenefitAlgorithmBase the optimiser aims to maximize the target function.
public class MyMaximizeTargetFunction : LargerValueIsBetterCostBenefitAlgorithmBase, IScriptingCostBenefitAlgorithm
{
// Constructor name must match class name
public MyMaximizeTargetFunction() : base()
{
...
}
- When the class derives from SmallerValueIsBetterCostBenefitAlgorithmBase the optimiser aims to minimize the target function.
public class MyMinimizeTargetFunction : SmallerValueIsBetterCostBenefitAlgorithmBase, IScriptingCostBenefitAlgorithm
{
// Constructor name must match class name
public MyMinimizeTargetFunction() : base()
{
...
}
Objects only available in optimisation
All the Toolbox functions and the Workbook is available inside this script though there are a couple of extra objects that are only available in this script type, namely:
Layout: this object that is an argument to the CalculateCostBenefitForLayout contains the layout that is being tested in the current optimiser iteration. It contains both the turbines that we are optimising along with any neighbouring/fixed turbines, though these do not change position between iterations.
- Contains a Name with the layout name
- Each LayoutTurbine is contained in list AllLayoutTurbines.
LayoutTurbine: this object represents a turbine location that is being tested in the current optimiser iteration. There is a loose coupling between this object and the turbine objects in the workbook, in that the optimiser does not change the latter.
- Contains a Name property with the turbine name
- Contains a Location property with the X and Y coordinates generated by the optimisation algorithm.
- Contains boolean properties IsInNeighbourFarm and IsMovableForOptimisation, first to indicate if its a subject turbine or not and second if its position changes in the optimisation process
Script initialisation options
It's possible to execute code that is only run once at the beginning of the optimisation process. This can be useful for defining constants for the whole optimisation process or reading configuration option from files.
The recommended location for placing this initialisation code is in the initialise method that is not on the template but can be created.
Its not recommended to put any code in the constructor, any code there will only be executed when the button Refresh from scripts in the Optimisation settings page is clicked. If this button is not pressed between optimisation runs then the code will not be executed.
public class MyCostBenefitAlgorithm : LargerValueIsBetterCostBenefitAlgorithmBase, IScriptingCostBenefitAlgorithm
{
public MyCostBenefitAlgorithm() : base()
{
// Add any one-off initialisation code here. This will only be executed once.
// Override the Initialise() function if you want code that gets called at the beginning of an optimisation calculation.
}
public override void Initialise()
{
// Write Any code to execute before an optimisation calculation, i.e runs only once before a full optimisation process
}
}
Target function
The target function is defined in method CalculateCostBenefitForLayout and is called on every optimisation iteration. It must have the defined signature which contains the following arguments:
- CancellationToken this object handles optimisation cancellation requests and the template structure should suffice to handle this.
- Layout as detailed above this object contains the turbine positions for the current optimisation iteration.
- turbineTypeNameForNewTurbines this parameter is necessary for the function that sets the layout to the workbook required for energy calculation. Its used in the symmetrical optimiser but needs to exist in the others as well.
- windFarmNameForNewTurbines this parameter is necessary for the function that sets the layout to the workbook required for energy calculation. Its used in the symmetrical optimiser but needs to exist in the others as well.
public SimpleCostBenefitValue CalculateCostBenefitForLayout(CancellationToken cancellationToken, Layout layout, string turbineTypeNameForNewTurbines, string windFarmNameForNewTurbines)
{
...
SimpleCostBenefitValue costBenefitValueToReturn = new SimpleCostBenefitValue();
if (!cancellationToken.IsCancellationRequested)
{
// TODO: Calculate your value and set it here
costBenefitValueToReturn.Value = 0.0;
}
return costBenefitValueToReturn;
}
public string CostBenefitValueName
{
get { return "<put cost benefit value name here>"; }
}
Object SimpleCostBenefitValue is the target function result. The value set in property Value will be what the optimiser will evaluate to decide if its a better or worse layout.
Defined the column name used to display the target value in the UI by setting the CostBenefitValueName property.
Maximize energy target in cost benefit script
In each iteration of the optimiser the optimisation algorithm will generate new layouts for testing the target function. These are stored in the layout object, the arguments turbineTypeNameForNewTurbines and windFarmNameForNewTurbines are also needed. WindFarmer will do all the management on matching the workbook turbines with the layout turbines.
The complex part is that we need to match the layout turbines that are of interest with the turbine results in scenario object returned from the energy calculation.
public SimpleCostBenefitValue CalculateCostBenefitForLayout(CancellationToken cancellationToken, Layout layout, string turbineTypeNameForNewTurbines, string windFarmNameForNewTurbines)
{
// First set the incoming layout to the workbook, i.e modify the workbook turbines to the new positions.
Toolbox.Optimisation.SetLayoutToWorkbook(layout, turbineTypeNameForNewTurbines, windFarmNameForNewTurbines);
// Now run the energy calculation to obtain the full AEP. This will use the settings defined in the workbook.
Scenario scenario = Toolbox.CalculateEnergy();
// Store the full yield of all turbines
double fullYieldOfAllTurbines = 0.0;
// We need to filter out the layout turbines that are not being optimised, i.e that belong to a neighbour wind farm.
foreach(LayoutTurbine turb in layout.AllLayoutTurbines.Where(t => t.IsInNeighbourFarm == false))
{
// match the results turbine with the layout turbine
ReadOnlyTurbine resTurb = scenario.Turbines.First(t => t.Name == turb.Name);
// Get the full yield for results turbine
double fullYieldMWh = scenario.TurbineTotalYields.GetVariantResult("Full").GetValueForTurbine(resTurb).Value / 1000; // convert to MWh
fullYieldOfAllTurbines += fullYieldMWh;
}
SimpleCostBenefitValue costBenefitValueToReturn = new SimpleCostBenefitValue();
if (!cancellationToken.IsCancellationRequested)
{
// the target function return value
costBenefitValueToReturn.Value = fullYieldOfAllTurbines;
}
return costBenefitValueToReturn;
}
// Set the column name for the target funtion value
public string CostBenefitValueName
{
get { return "Full yield all turbines [MWh]"; }
}
Report extra values from the optimisation process
Fuction CalculateCostBenefitForLayout requires a SimpleCostBenefitValue to be returned, this contains the target function that will be evaluated by the optimiser. In addition its possible to report other individual values for each iteration.
For example, taking the energy calculation example we could report each individual turbine full yield for each iteration. The optimiser would add corresponding columns in the UI for each different report value.
public SimpleCostBenefitValue CalculateCostBenefitForLayout(CancellationToken cancellationToken, Layout layout, string turbineTypeNameForNewTurbines, string windFarmNameForNewTurbines)
{
Toolbox.Optimisation.SetLayoutToWorkbook(layout, turbineTypeNameForNewTurbines, windFarmNameForNewTurbines);
Scenario scenario = Toolbox.CalculateEnergy();
double fullYieldOfAllTurbines = 0.0;
// The object to return shoudl be created earlier since it will contain the other report values
SimpleCostBenefitValue costBenefitValueToReturn = new SimpleCostBenefitValue();
foreach(LayoutTurbine turb in layout.AllLayoutTurbines.Where(t => t.IsInNeighbourFarm == false))
{
ReadOnlyTurbine resTurb = scenario.Turbines.First(t => t.Name == turb.Name);
double fullYieldMWh = scenario.TurbineTotalYields.GetVariantResult("Full").GetValueForTurbine(resTurb).Value / 1000; // convert to MWh
fullYieldOfAllTurbines += fullYieldMWh;
// Report the full yield for each turbine by adding a name value pair
costBenefitValueToReturn.NameValuePairs.Add(new CostBenefitNameValuePair() { Name = string.Format("{0} Full Yield [MWh]", turb.Name), Value = fullYieldMWh});
}
if (!cancellationToken.IsCancellationRequested)
{
// the target function return value
costBenefitValueToReturn.Value = fullYieldOfAllTurbines;
}
return costBenefitValueToReturn;
}
// Set the column name for the target funtion value
public string CostBenefitValueName
{
get { return "Full yield all turbines [MWh]"; }
}
Distance penaly weighted target using Toolbox and Workbook
In this section we'll look at how we can do an energy distance penalty optimisation algorithm. We'll use the site center as the reference location.
public SimpleCostBenefitValue CalculateCostBenefitForLayout(CancellationToken cancellationToken, Layout layout, string turbineTypeNameForNewTurbines, string windFarmNameForNewTurbines)
{
// Define a reference location at the site center
Location siteCenterWgs84 = new Location(Workbook.Geography.SiteInformation.Longitude, Workbook.Geography.SiteInformation.Latitude);
uint epsgWgs84 = 4326;
Location refLocation = (Location)Toolbox.ReprojectPoint(siteCenterWgs84, Toolbox.GetProjectionFromEpsgCode(epsgWgs84), Workbook.Geography.Projection);
// A 10% penalty for every 1 km away from the reference location
double penaltyDistance = 1000.0;
double penaltyEnergy = 0.1;
Toolbox.Optimisation.SetLayoutToWorkbook(layout, turbineTypeNameForNewTurbines, windFarmNameForNewTurbines);
Scenario scenario = Toolbox.CalculateEnergy();
double fullYieldWithPenaltyOfAllTurbines = 0.0;
// The object to return should be created earlier since it will contain the other report values
SimpleCostBenefitValue costBenefitValueToReturn = new SimpleCostBenefitValue();
foreach(LayoutTurbine turb in layout.AllLayoutTurbines.Where(t => t.IsInNeighbourFarm == false))
{
ReadOnlyTurbine resTurb = scenario.Turbines.First(t => t.Name == turb.Name);
double fullYieldMWh = scenario.TurbineTotalYields.GetVariantResult("Full").GetValueForTurbine(resTurb).Value / 1000; // convert to MWh
// Apply the penaly
double distance = Math.Sqrt(Math.Pow(turb.Location.X - refLocation.X , 2) + Math.Pow(turb.Location.Y - refLocation.Y, 2));
double penalty = (distance / penaltyDistance) * penaltyEnergy;
double fullYieldWithPenalty = fullYieldMWh * Math.Max(1 - penalty, 0.0);
fullYieldWithPenaltyOfAllTurbines += fullYieldWithPenalty;
// Report the full yield for each turbine by adding a name value pair
costBenefitValueToReturn.NameValuePairs.Add(new CostBenefitNameValuePair() { Name = string.Format("{0} Penalty Full Yield [MWh]", turb.Name), Value = fullYieldWithPenalty});
// Report the penalty applied
costBenefitValueToReturn.NameValuePairs.Add(new CostBenefitNameValuePair() { Name = string.Format("{0} Penalty [%]", turb.Name), Value = Math.Round(penalty * 100.0, 1)});
}
if (!cancellationToken.IsCancellationRequested)
{
// the target function return value
costBenefitValueToReturn.Value = fullYieldWithPenaltyOfAllTurbines;
}
return costBenefitValueToReturn;
}
// Set the column name for the target funtion value
public string CostBenefitValueName
{
get { return "Full yield all turbines with penalty[MWh]"; }
}
We could use a distance to a mast M3 instead of the site center by accessing its location in the workbook.
Location refLocation = (Location)Workbook.Climate.MeasurementSites["M3"].Location;