Edit on Gitlab Launch with Binder

Site

The Site object provides the free wind conditions at the site including mean wind, shear, turbulence etc.

Current implementations are:

  • TurbulenceFieldSite

TurbulenceFieldSite

[1]:
# imports and default values
import numpy as np
import matplotlib.pyplot as plt
from py_wake.utils.plotting import setup_plot
from dynamiks.sites import TurbulenceFieldSite
from dynamiks.sites.turbulence_fields import MannTurbulenceField, RandomTurbulence
from dynamiks.sites.mean_wind import ConstantWindSpeed
from dynamiks.views import ZView, XYView
from dynamiks.utils.test_utils import DefaultDWMFlowSimulation

ws = 10
ti = 0.1
no_turb_field = RandomTurbulence(ti=0, ws=ws) # turbulence field without turbulence

The TurbulenceFieldSite implements a site defined by a wind speed (object) and a turbulence field

[2]:
help(TurbulenceFieldSite.__init__)
Help on function __init__ in module dynamiks.sites._site:

__init__(self, ws, turbulenceField, turbulence_offset=[0, 0, 0], turbulence_transport_speed=None)
    Site with mean wind and turbulence field

    Parameters
    ----------
    ws : int, float or MeanWind-object
        mean wind speed or MeanWind-object, e.g. ConstantWindSpeed
    turbulenceField : TurbulenceField
        E.g. MannTurbulenceField
    turbulence_offset : tuple(x,y,z), optional
        turbulence (x,y,z)-offset. Default is no offset
    turbulence_transport_speed : int, float or None, optional
        Turbulence field advection speed.
        If None (default), the speed is obtained from the mean wind speed

Mean wind

The mean wind argument can be either a constant uniform mean wind speed value

[3]:
site = TurbulenceFieldSite(ws, no_turb_field)

or a ConstantWindSpeed object

[4]:
const_ws = ConstantWindSpeed(ws=10)
site = TurbulenceFieldSite(const_ws, no_turb_field)

Shear

The latter ConstantWindSpeed option allows specification of vertical shear in terms of a PyWake shear object, e.g. PowerShear or LogShear

[5]:
import numpy as np
from py_wake.site.shear import PowerShear, LogShear


# PyWake shears
power_shear = PowerShear(h_ref=100, # reference height
                   alpha=0.1) # power shear exponent

log_shear = LogShear(h_ref=100, # reference height
                 z0=0.03)   # roughness length

view = ZView(x=0,y=0, z=np.linspace(1,200))

for shear in [power_shear, log_shear]:
    const_ws = ConstantWindSpeed(ws=10, shear=shear)
    site = TurbulenceFieldSite(const_ws, no_turb_field)
    u,v,w = site.get_windspeed(view)
    plt.plot(u, view.z, label=shear.__class__.__name__)
setup_plot(xlabel='Wind speed [m/s]', ylabel='Height [m]')
../_images/notebooks_Site_10_0.png

TurbulenceField

Current implementations

  • RandomTurbulence

  • MannTurbulenceField

RandomTurbulence

The RandomTurbulence produces random turbulence. It is very fast but mainly usable for tests

[6]:
from dynamiks.sites.turbulence_fields import RandomTurbulence
site = TurbulenceFieldSite(10, RandomTurbulence(ti=ti, ws=ws))
[7]:
uvw = site.get_windspeed(view)
for ws, n in zip(uvw, 'uvw'):
    plt.plot(ws, view.z, label=n)
setup_plot(xlabel='Wind speed [m/s]', ylabel='Height [m]')
../_images/notebooks_Site_14_0.png

MannTurbulenceField

The MannTurbulenceField extends the hipersim.MannTurbulenceField from (see https://hipersim.pages.windenergy.dtu.dk/hipersim/MannTurbulenceField.html).

The turbulence can be generated via Hipersim and/or read from a netcdf or the tree-file binary Mann turbulence format.

The turbulence field size should cover the whole wind farm in all dimensions and time:

  • Lx = Nxdx: Farm length in direction of wind + Simulation time turbulence transport speed.

  • Ly = Ny*dy: Farm width (perpendicular to the wind)

  • Lz = Nz*dz: from ground to blade tip height

The needed grid resolution depends on the wind turbine model. For HAWC2 (actuator line), Liew (2022) recommends a resolution of D/50.

In a study of the Lillgrund wind farm (Liew 2023), a turbulence field with 38880x1792x64 grid points is used. The size of this field is 50Gb (3 uvw components and 32bit floats). Note, that such big files usually requires a HPC system, as personal computers have far less memory.

Reference:

Generate turbulence field via Hipersim

Use the MannTurbulenceField.generate method to generate new turbulence fields. See documentation

[8]:
from dynamiks.sites.turbulence_fields import MannTurbulenceField
mtf = MannTurbulenceField.generate(alphaepsilon=.1, # use correct alphaepsilon or scale later
                                   L=33.6, # length scale
                                   Gamma=3.9, # anisotropy parameter
                                   Nxyz=(1024,64,64), # should be large enough to cover whole farm in all dimensions and time, see above
                                   dxyz=(2,2,2), # should be small enough to capture variations needed for the wind the turbine model
                                   seed=1, # seed for random generator
                                   HighFreqComp=0, # the high frequency compensation is questionable and it is recommened to switch it off
                                   double_xyz=(False, False, False), # turbulence periodicity is not expected to be an issue in a wind farm
                                  )

You can save the generated turbulence field for later use, see Hipersim documentation

[9]:
mtf.to_netcdf(filename=None)

If filename=None (default), the name is autogenerated from the properties. In this case the filename is Hipersim_mann_l33.6_ae0.1000_g3.9_h0_1024x64x64_2.000x2.00x2.00_s0001.nc

Read turbulence field from file

When saving and loading Hipersim turbulence file, it is recommeneded to use the netcdf format as all data and attributes are stored in one file.

[10]:
mtf = MannTurbulenceField.from_netcdf('Hipersim_mann_l33.6_ae0.1000_g3.9_h0_1024x64x64_2.000x2.00x2.00_s0001.nc')
[11]:
mtf.to_xarray()
[11]:
<xarray.DataArray (uvw: 3, x: 1024, y: 64, z: 64)> Size: 50MB
array([[[[-0.35261983, -0.08925066, -0.23356584, ..., -1.1009454 ,
          -0.8976475 , -0.76379365],
         [-0.9250733 , -0.4931373 , -1.1856041 , ..., -0.7663951 ,
          -0.7959621 , -1.1620626 ],
         [-0.8972635 , -0.94220793, -1.0300303 , ..., -0.77264917,
          -0.5917986 , -0.7867458 ],
         ...,
         [-0.6236845 , -0.5403369 , -0.41061032, ..., -0.15890206,
          -0.06358869, -0.01130451],
         [-0.38042337, -0.46187547, -0.43752527, ..., -1.1627533 ,
          -0.7095258 , -0.2183148 ],
         [-0.6119078 ,  0.02988166,  0.13592918, ..., -1.3156966 ,
          -0.8855437 , -0.66417706]],

        [[-0.19947052, -0.3182267 , -0.037643  , ..., -1.2702348 ,
          -0.89583623, -0.6222654 ],
         [-0.9250941 , -0.7349631 , -0.55023694, ..., -0.95171165,
          -0.67152786, -0.673831  ],
         [-1.3296094 , -1.0069457 , -0.695421  , ..., -0.9798774 ,
          -1.1212653 , -0.9459772 ],
...
           0.16375346,  0.14584586],
         [ 0.40218484,  0.20448992,  0.2411005 , ...,  0.6785034 ,
           0.4723086 ,  0.6304167 ],
         [ 0.2964676 ,  0.43353313,  0.37929705, ...,  0.6079931 ,
           0.48515218,  0.39158955]],

        [[ 0.7356683 ,  0.46824086, -0.03418609, ...,  0.47688082,
           0.55650324,  0.6406135 ],
         [ 0.44476473, -0.13761413, -0.32461864, ...,  0.37383848,
           0.2806024 ,  0.34730986],
         [-0.11733318, -0.36337373, -0.18907116, ...,  0.26693332,
          -0.17221646, -0.19901314],
         ...,
         [-0.15419303,  0.00421654,  0.08828714, ...,  0.27103174,
           0.35762107,  0.00870521],
         [ 0.3831636 ,  0.09581088,  0.22962908, ...,  0.7461332 ,
           0.7619566 ,  0.6896519 ],
         [ 0.5637457 ,  0.29805893,  0.32872304, ...,  0.67402774,
           0.84536624,  0.88036865]]]],
      shape=(3, 1024, 64, 64), dtype=float32)
Coordinates:
  * uvw      (uvw) <U1 12B 'u' 'v' 'w'
  * x        (x) int64 8kB 0 2 4 6 8 10 12 ... 2036 2038 2040 2042 2044 2046
  * y        (y) int64 512B 0 2 4 6 8 10 12 14 ... 114 116 118 120 122 124 126
  * z        (z) int64 512B 0 2 4 6 8 10 12 14 ... 114 116 118 120 122 124 126
Attributes:
    double_xyz:    [0 0 0]
    name:          Hipersim_mann_l33.6_ae0.1000_g3.9_h0_1024x64x64_2.000x2.00...
    alphaepsilon:  0.1
    L:             33.6
    Gamma:         3.9
    HighFreqComp:  0
    Generator:     Hipersim
    seed:          1

It is also possible to save and load the tree-file binary Mann turbulence format, see Hipersim documentation but the netcdf format is recommended as you do not risk to mix up files and/or attributes

Turbulence intensity and scaling

When dealing with turbulence intensity, \(TI=U/u'\), of turbulence fields used for wind farms, some additional things need to be considered:

  • \(U\) is the mean wind speed, but where and when, and does it include wakes

  • \(u'\) is the fluctuations, but how long is the measurement period and what is the cut-off frequency of the measurement device

  • Are you interested in the turbulence intensity of the theoretical Mann model spectrum or the actual turbulence field realization (in a point, an area or the whole turbulence field).

  • Are \(U\) and \(u'\), the longitudinal, horizontal or total speeds

For a given mean wind speed, U, measurement period, T, and cut-off frequency, cutoff_frq, the turbulence intensity of the theoretical Mann model spectrum can be obtained by:

[12]:
U = 10
T = 600
cutoff_frq = 10
mtf.spectrum_TI(U=U, T=T, cutoff_frq=cutoff_frq)
[12]:
np.float64(0.14324855863543767)

Scaling the turbulence field, such that the underlying theoretical Mann model spectrum has a specific turbulence intensity is done with the scale_TI method:

[13]:
print (f'Before: Box TI={mtf.uvw[0].std()/U:.3f}, alphaepsilon:{mtf.alphaepsilon:.3f}, theoretical spectrum TI {mtf.spectrum_TI(U):.2f}')
mtf.scale_TI(TI=0.1, U=10,  T=600, cutoff_frq=10)
print (f'After: Box TI={mtf.uvw[0].std()/U:.3f}, alphaepsilon:{mtf.alphaepsilon:.3f}, theoretical spectrum TI {mtf.spectrum_TI(U):.2f}')
Before: Box TI=0.129, alphaepsilon:0.100, theoretical spectrum TI 0.13
After: Box TI=0.090, alphaepsilon:0.049, theoretical spectrum TI 0.09
[14]:
from dynamiks.sites.turbulence_fields import RandomTurbulence
site = TurbulenceFieldSite(ws=10, turbulenceField=mtf)
[15]:
view = ZView(x=0,y=0, z=np.linspace(1,200))
uvw = site.get_windspeed(view)
for ws, n in zip(uvw, 'uvw'):
    plt.plot(ws, view.z, label=n)
setup_plot(xlabel='Wind speed [m/s]', ylabel='Height [m]')
../_images/notebooks_Site_31_0.png

Turbulence offset

The turbulence_offset argument of TurbulenceFieldSite specifies the turbulence field origo at time=0

[16]:
view = XYView(z=100)
axes = plt.subplots(2,1, figsize=(8,5))[1]
for offset, ax in zip([(0,0,0), (200,100,0)], axes):
        site = TurbulenceFieldSite(ws=10, turbulenceField=mtf, turbulence_offset = offset)
        fs = DefaultDWMFlowSimulation(site=site)
        uvw = fs.get_windspeed(view, include_wakes=False, xarray=True)
        uvw.sel(uvw='u').plot(ax=ax)
        ax.axis('scaled')
        ax.set_xlim([-50,1000])
        ax.set_ylim([0,250])
        ax.set_title(f'Offset: {offset}, time: 0')
../_images/notebooks_Site_33_0.png

Turbulence transport speed

The turbulence_transport_speed argument of TurbulenceFieldSite specifies the advection speed of the turbulence field

[17]:
view = XYView(z=100)
axes = plt.subplots(2,1, figsize=(8,5))[1]
for transport_speed, ax in zip([10,20], axes):
        site = TurbulenceFieldSite(ws=10, turbulenceField=mtf, turbulence_transport_speed=transport_speed)
        fs = DefaultDWMFlowSimulation(site=site)
        fs.run(10) # run 10s
        uvw = fs.get_windspeed(view, include_wakes=False, xarray=True)
        uvw.sel(uvw='u').plot(ax=ax) # plot u component
        ax.axis('scaled')
        ax.set_xlim([-50,1000])
        ax.set_ylim([0,250])
        ax.set_title(f'Transport speed: {transport_speed}m/s, time: {fs.time}s')
../_images/notebooks_Site_35_0.png

LES precursor

Precursors are typically turbulence field extracted from CFD LES simulations, i.e. simulations where the turbulence has dimenstions \((uvw, time, x, y, z)\).

Often precursor fields are sampled at a plane some distance upstream of the wind farm or wind turbine, which reduces the dimensions to \((uvw, time,y,z)\).

These can be used in Dynamiks similar to normal turbulence fields, but we need to convert the time axis into x coordinate and assume Taylor’s frozen turbulence hypothesis, to obtain turbulence in other areas of the domain.

[18]:
import warnings
import xarray as xr
import numpy as np
from numpy import newaxis as na
import matplotlib.pyplot as plt
from dynamiks.sites import TurbulenceFieldSite
from dynamiks.utils.test_utils import DefaultDWMFlowSimulation, tfp # test_files path
from dynamiks.sites.turbulence_fields import TurbulenceField
from dynamiks.views import XYView, XZView, MultiView
from hipersim import Bounds
from py_wake.utils.plotting import setup_plot

First we load the precursor, in this case a downsampled precursor obtained from Ellipsys.

[19]:
da = xr.load_dataarray(tfp + "precursor.nc")
da.sel(vel='u').isel(y=0).plot(x='t')
../_images/notebooks_Site_40_0.png

To map the time axis into x-coordinates, we need an advection speed. It can specified or extracted as the mean wind speed at a specific height. In this case we take the wind speed at \(z=100m\)

[20]:
U_z = da.sel(vel='u').mean(('y', 't'))
advection_speed = U_z.interp(z=100).item()
[21]:
U_z.plot(y='z')
plt.plot(advection_speed,100,'.', label=f'U(z=100) = {advection_speed:.1f}m/s')
setup_plot(xlabel='U mean [m]', ylabel='height [m]', figsize=(4,3))
../_images/notebooks_Site_43_0.png

Having the advection speed, we can find the distance between time samples

[22]:
dt, dy, dz = [np.diff(v[:2])[0] for v in [da.t, da.y, da.z]]
dx = dt * advection_speed

We can now instantiate a site with the precursor turbulence and plot it

[23]:
def get_flowSimluation(bounds=Bounds.Error, offset=(0,0,0), da=da):
    Nxyz = Nx,Ny,Nz = da.shape[1:] # first dimension is uvw
    tf = TurbulenceField(da.values[:,::-1], # reverse x axis to move precursor forward
                         Nxyz=Nxyz, dxyz=[dx, dy, dz], bounds=bounds)
    site = TurbulenceFieldSite(ws=0, turbulenceField=tf, turbulence_transport_speed=advection_speed,
                               turbulence_offset=np.array([-(Nx-1)*dx,0,0]) + offset) # add box length to count offset from box front
    return DefaultDWMFlowSimulation(x=[0], y=[0], site=site)

fs = get_flowSimluation(Bounds.Repeat)
ax1,ax2 = plt.subplots(2,1, figsize=(12,6))[1]
fs.show(MultiView([XYView(ax=ax1, xlim=[-5500,500], ylim=[-100,900]), XZView(y=0, ax=ax2)]))
../_images/notebooks_Site_47_0.png

The turbine is located at \((0,0)\), so half of the rotor and all wake particles are outside the precursor field. Before running the simulation we need to consider how to deal with areas outside the box. There are different options:

Offset box

We can offset the precursor field such that it covers the turbine and all wake particles during the whole simulation.

[24]:
fs = get_flowSimluation(offset=(2000,-400,0), bounds=Bounds.Error)
ax1,ax2 = plt.subplots(2,1, figsize=(12,6))[1]
fs.visualize(10, MultiView([XYView(ax=ax1), XZView(y=0, ax=ax2)]))
../_images/notebooks_Site_50_0.png

Repeat or mirror the box

In the visualization below, the box is repeated in the x direction and mirrored in the y and z direction.

Note:

  • If the box is not periodic, repeating it may lead to discontinuities, as seen in the x direction near the turbine

  • If the box is not homogeneous, mirroring or repeating it leads to wrong behavour as seen below where the low wind speed near the ground is applied again around \(z=900\)

[25]:
fs = get_flowSimluation(bounds=[Bounds.Repeat,Bounds.Mirror,Bounds.Mirror])
fs.visualize(10, view=XZView(y=0, x=np.linspace(-1000,1000), z=np.linspace(0,910)))
../_images/notebooks_Site_52_0.png

Use Edge values

The bounds option Bounds.Warning gives a warning when requesting wind speed outside the box and returns the nearest valid value, i.e. the wind speed at the edge of the box.

[26]:
with warnings.catch_warnings(action='ignore'):
    fs = get_flowSimluation(bounds=[Bounds.Warning,Bounds.Warning,Bounds.Warning])
    fs.visualize(10, view=XZView(y=0, x=np.linspace(-1000,1000), z=np.linspace(0,910)))
../_images/notebooks_Site_54_0.png

Extend the box

In case you know a better estimate of the wind speed outside the box, the wind field can be extended.

Below, the wind speed in 50 time steps in front of the box is set to the mean \(uvw(z)\) profile.

[27]:
da_mean = da[:,:50]*0+da.mean(('y','t')) # 50 time steps of uvw(z) profile
coords = dict(vel=['u','v','w'], t=np.arange(0,200)*dt, y=da.y.values, z=da.z.values)
da_ext = xr.DataArray(np.concatenate([da_mean, da],1), dims=da.dims,coords=coords)
fs = get_flowSimluation(da=da_ext, offset=(50*dx,-200,0))
fs.visualize(10, view=XZView(y=0, x=np.linspace(-1000,1000)))
../_images/notebooks_Site_56_0.png
[ ]: