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]')
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]')
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:
Jaime Liew and Gunner Chr. Larsen 2022 J. Phys.: Conf. Ser. 2265 032049, https://doi.org/10.1088/1742-6596/2265/3/032049)
Jaime Liew et al 2023 J. Phys.: Conf. Ser. 2626 012050
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: 1It 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]')
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')
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')
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')
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))
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)]))
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)]))
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)))
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)))
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)))
[ ]: