Optimize Calculation Service

The new optimizer in windPRO is also available via scripting (the previously used optimizer is not available). The optimizer is designed for the use in the GUI and features possibilities for parallel calculations and a handy hierarchy for trying out different wind turbine types, resource grids, and other constraints. This interactivity makes the interaction with scripting more complex compared to other calculation services.

The optimizer has a hierarchy structure of:

Data types available from online

Session

Overall container holding multiple sites

Sites

Selection of wind resource and wtg area

WTG

A wind trubine type to use

Size

number Of turinbes allowed

Layout

Layout realized

Each of these layers is identifiable via unique identifiers as properties called “Guid”. In principle there can be multiple layouts per size. Sessions, sites, and wtgs can all be generated with an Add method. Data can be accessed with a Get metho and send back to windPRO with a Set method. Size and Layout are the exception that are generate with CreateDefaultLayouts both are the same time. They cannot be changed again with a set method but have a get method to access the data.

Running the optimization

Compared to other calculations the optimizer can run asynchronous, meaning you can start the optimization and and at the same time work within the optimizer. Therefore, calculations can be started but for determining when they are done it is necessary to check their status. Multiple optimizations can be started at the same time in windPRO.

Optimizing on LCOE and NPV

For NPV and LCOE calculation a turbine cost model needs to be defined. This is possible in windPRO but not fully included in the scripting interface. At present the ID for the cost model is simply the position in the list of added cost models. It is possible to use this in scripting, but there is no way of validating which cost model is used or how they are set up.

The script below gives an example of automatically setting up an optimization from a prepared project. For this project all resource files and a wtg area are already defined but they can be set up with scripting as well.

"""
Copyright 2023 EMD International
License for this script: MIT https://opensource.org/license/mit/
License for windPRO commercial software: https://www.emd-international.com/contact-us/general-terms-conditions-sale/
"""

import os
import time
from windproapi import WindProApi, nan_to_skipvalue
from windproapi.utils import get_windpro_sample_path
import numpy as np
import pandas as pd

# Opening windPRO
_windproapi = WindProApi()
working_dir = os.path.join(get_windpro_sample_path('4.0'), 'Aparados da Serra', '4.0')
project_path = os.path.join(working_dir, 'Aparados da Serra basic project.w40p')

_windproapi.start_windpro_random_port()

# Services
project_service = _windproapi.get_service('ProjectService')
objects_service = _windproapi.get_service('ObjectsService')
calculation_service = _windproapi.get_service('CalculationService')
optimize_service = _windproapi.get_service('OptimizeService')
wtg_explorer_service = _windproapi.get_service('WtgExplorerService')
factory = _windproapi.get_factory('WindproService')

# Loading project file
project_service.LoadFromFile(filename=project_path)

# Setting up an optimization for multiple sizes as potential layouts.
# Choosing range of number of turbines to be investigated and a resource grid as the basis
N_turbines = np.arange(1, 15, 1)
resource_file = os.path.join(working_dir, 'Aparados da Serra basic project_Hub_98.0_0.rsf')

# Making new session
session_name = 'New Session from scripting'
session_guid = optimize_service.AddSession(session_name)
optimize_service.OpenSession(session_guid)

# Adding a new site to the calculations
site_uid = optimize_service.AddSite('Test Optimization')
site = optimize_service.GetSite(site_uid)

# Here we can decide to change the wake decay constant to user defined
site.WakeDecayDefTerID = 'wkCustom'
site.UserDefWakeDecay = 0.07

# Setting the site specific values.
# We stay with the default onshore wake decay constant which is default behaviour
wtg_area_obj = objects_service.GetObjects('WTGareas')[0]
site.WTGAreaHandle = wtg_area_obj.Handle
site.WindResourceFiles.string[0] = resource_file
nan_to_skipvalue(site)
optimize_service.SetSite(site)

# Adding specific wind turbine
wtg_from_explorer = wtg_explorer_service.GetWtgFromUID(uid='{832CAEAF-54E2-4A29-842C-144AA88B553E}')
# Setting default tower height to match the rsf file
wtg_from_explorer.DefHubHeight = 98.0
wtg_uid = optimize_service.AddWTG(siteGuid=site_uid,
                                  name=os.path.basename(wtg_from_explorer.FileName),
                                  wtgFile=wtg_from_explorer.FileName)

# Adding different turbine sizes
for n in N_turbines:
    print(f'Adding {n} turbines size.')
    SizesAndRuns = optimize_service.CreateDefaultLayout(wtgGuid=wtg_uid,
                                                        numTurbines=n)
wtg = optimize_service.GetWTG(wtg_uid)

layout_list = factory.TApiStringArray()
for i in range(len(N_turbines)):
    # Getting the Size to get the Guid for the layout for starting the optimization later
    new_size = optimize_service.GetSize(wtg.RunList.TApiOptimizeNameGuid[i].Guid)
    layout_list.string.append(new_size.LayoutList.TApiOptimizeNameGuid[0].Guid)

# Starting runs
startRuns = optimize_service.StartRuns(layout_list)
# Runs are running in the background in the optimizer.
# Therefore, we need to check the status of all
is_running = True
while is_running:
    is_running = False
    calcs_running = 0
    time.sleep(10)
    for layout_guid in layout_list.string:
        status = optimize_service.GetStatus(layout_guid)
        if status == 'olsApiRunning':
            is_running = True
            calcs_running += 1
    print(f'Number of calculations running: {calcs_running}')

# Printing all AEPs found and the additional AEP for adding one more turbine in an optimized setup
res_dict = {'AEP': [], 'delta AEP': [], 'Guid': []}
last_AEP = 0.0
print('  N_Turbines|         AEP|   delta AEP')
print('-' * 12 + '+' + '-' * 12 + '+' + '-' * 12)
for n, layout_guid in zip(N_turbines, layout_list.string):
    layout = optimize_service.GetLayout(layout_guid)
    aep, delta_aep = layout.MaxObjective, layout.MaxObjective - last_AEP
    print('{:12d}|{:12.2f}|{:12.2f}'.format(n, aep, delta_aep))
    last_AEP = layout.MaxObjective
    res_dict['AEP'].append(aep)
    res_dict['delta AEP'].append(delta_aep)
    res_dict['Guid'].append(layout.Guid)

# Make dataframe with AEPs and GUId
df = pd.DataFrame(res_dict, index=N_turbines)
df.index.name = 'N_turbines'

# As an example selecting the realization where an additional turbine is adding 85% of the AEP the smallest setup
df['rel delta AEP'] = df['delta AEP'] / (df['AEP'][N_turbines[0]] / 2)
ind = (df['rel delta AEP'] - 0.85).abs().idxmin()

# Getting the layout
layout = optimize_service.GetLayout(df.loc[ind]['Guid'])
# Realise layout and return handles for WTG's placed
WTGHandles = optimize_service.ApiRealiseLayout(layoutGuid=layout.Guid,
                                               layerName='Realized Layout')

optimize_service.CloseSession(True)