Custom wind speed and sig profiles

This example shows how you can create your own functions for the wind speed and turbulence standard deviation as a function of spatial location. For simplicity, we will keep the default Kaimal spectrum, but you could also have the spectrum be a custom function as well.

The spatial variation of the mean wind speed and turbulence standard deviation we choose in this example will be to mimic a full-wake scenario. Note that these variables are not necessarily physically realistic, this is just a demonstration!

This example has the following sections:

Note: it is also possible to have a custom power spectrum profile function, but that is not explicitly demonstrated here. However, the method is extremely similar to what is shown here, and more information can be found in the related tab under the Reference Guide.

Preliminaries: importing functions

Before we can begin, we need to importe a few functions and define some variables that we will need later.

[1]:
%matplotlib inline
#import os

import matplotlib.pyplot as plt  # matplotlib for some plotting
import numpy as np  # numeric python functions

from pyconturb import gen_turb, gen_spat_grid  # generate turbulence, rect. grid
from pyconturb.wind_profiles import constant_profile  # useful for freestream

cntr_pos, rad, u_inf = [0, 119], 90, 12  # center pos of grid, radius of "wake", and inflow wsp

Define turbulence grid and time

Our first step is to define 1) the simulation time/time step and 2) the points of the grid and which turbulence components (i.e., \(u\), \(v\) and/or \(w\)) we want to simulate. For this example, we’ll pick a fairly dense grid, but we’ll only simulate the longitudinal component.

[2]:
T, nt = 300, 300  # only simulate 300 seconds for now
y, z = np.linspace(-rad, rad, 15), np.linspace(cntr_pos[1] - rad, cntr_pos[1] + rad, 15)
spat_df = gen_spat_grid(y, z, comps=[0])  # generate grid with only u
print(spat_df.head())  # show a few columns of the spatial dataframe
print('No. of points to simulate: ', spat_df.shape[0])  # number of points to simulate
plt.scatter(spat_df.loc['y'], spat_df.loc['z'])  # show the grid
plt.axis('equal');
   u_p0       u_p1       u_p2       u_p3       u_p4       u_p5        u_p6  \
k   0.0   0.000000   0.000000   0.000000   0.000000   0.000000    0.000000
x   0.0   0.000000   0.000000   0.000000   0.000000   0.000000    0.000000
y -90.0 -90.000000 -90.000000 -90.000000 -90.000000 -90.000000  -90.000000
z  29.0  41.857143  54.714286  67.571429  80.428571  93.285714  106.142857

    u_p7        u_p8        u_p9  ...     u_p215      u_p216  u_p217  \
k    0.0    0.000000    0.000000  ...   0.000000    0.000000     0.0
x    0.0    0.000000    0.000000  ...   0.000000    0.000000     0.0
y  -90.0  -90.000000  -90.000000  ...  90.000000   90.000000    90.0
z  119.0  131.857143  144.714286  ...  93.285714  106.142857   119.0

       u_p218      u_p219      u_p220      u_p221      u_p222      u_p223  \
k    0.000000    0.000000    0.000000    0.000000    0.000000    0.000000
x    0.000000    0.000000    0.000000    0.000000    0.000000    0.000000
y   90.000000   90.000000   90.000000   90.000000   90.000000   90.000000
z  131.857143  144.714286  157.571429  170.428571  183.285714  196.142857

   u_p224
k     0.0
x     0.0
y    90.0
z   209.0

[4 rows x 225 columns]
No. of points to simulate:  4
../_images/notebooks_2_custom_profiles_4_1.png

Custom wind speed profile

Now let’s define our custom functions for the spatial variation of the mean wind speed. Note that the wind speed function must be of the form

wsp_values = wsp_func(spat_df, **kwargs)

where kwargs is a dictionary of the keyword arguments for the profile function. (It can also include unused keyword arguments.)

[3]:
def wake_wsp(spat_df, cntr_pos=[0, 90], rad=50, u_inf=10, max_def=0.5, **kwargs):
    """Non-realistic wake deficit.
    rad is the wake of the wake, u_inf is undisturbed inflow, and max_def is the max. deficit."""
    y, z = spat_df.loc[['y', 'z']].values
    dist_to_cntr = np.sqrt((y - cntr_pos[0])**2 + (z - cntr_pos[1])**2)  # distance to center point
    freestream = constant_profile(spat_df, u_ref=u_inf)  # no wake deficit
    wake_def = max_def * np.sin(np.pi/2 * (rad - dist_to_cntr) / rad)  # sinusoidal
    wake_def = wake_def * (dist_to_cntr < rad)  # deficit is not outside of rad
    return np.array(freestream - wake_def)  # must return array regardless of input

We can plot a few different waked profiles to check that this function works.

[4]:
fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(9.9, 8))
plot_vals = [([0, 119], 90, 0.5), ([0, 119], 30, 0.5), ([-30, 80], 90, 0.5), ([60, 130], 90, 2)]
for iax, (cnt, r, mdef) in enumerate(plot_vals):
    ax = axs[iax//2, iax%2]
    wsp_values = wake_wsp(spat_df, cntr_pos=cnt, rad=r, u_inf=u_inf, max_def=mdef)
    cnt = ax.contourf(y, z, wsp_values.reshape(y.size, z.size).T)
    ax.axis('equal')
    plt.colorbar(cnt, ax=ax);
    ax.set_title(f'{rad}-m rad at {cntr_pos}, max_def={mdef}')
../_images/notebooks_2_custom_profiles_8_0.png

Custom turbulence standard deviation profile

Just like we did for the mean wind speed, now let us define a function for the turbulence standard deviation as a function of spatial location (and turbulence component). Note that the sig function must be of the form

sig_values = sig_func(spat_df, **kwargs)

where kwargs is a dictionary of the keyword arguments for the profile function. (It can also include unused keyword arguments.)

[5]:
def wake_sig(spat_df, cntr_pos=[0, 90], rad=50, sig_inf=1.2, max_perc=0.20, **kwargs):
    """Non-realistic wake turbulence. sig_inf is the undisturbed standard deviation and max_perc is the
    maximum percentage increase of sigma at the center."""
    y, z = spat_df.loc[['y', 'z']].values
    dist_to_cntr = np.sqrt((y - cntr_pos[0])**2 + (z - cntr_pos[1])**2)  # distance to center point
    mask = (dist_to_cntr < rad)  # points that fall within the wake
    wake_sig = sig_inf * constant_profile(spat_df, u_ref=1)  # freestream sig
    wake_sig[mask] += max_perc*sig_inf * np.sin(np.pi/2 * (rad - dist_to_cntr[mask]) / rad)
    return np.array(wake_sig)  # must return array regardless of input
[6]:
fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(10, 3.7))
plot_vals = [([-60, 160], 1.2, 0.2), ([10, 100], 3.5, 0.4)]
for iax, (cntr, sig, mperc) in enumerate(plot_vals):
    ax = axs[iax]
    sig_values = wake_sig(spat_df, cntr_pos=cntr, rad=rad, sig_inf=sig,
                          max_perc=mperc)
    cnt = ax.contourf(y, z, sig_values.reshape(y.size, z.size).T)
    ax.axis('equal')
    plt.colorbar(cnt, ax=ax);
    ax.set_title(f'Max +{mperc*100}% at {cntr}, sig_inf={sig}')
../_images/notebooks_2_custom_profiles_11_0.png

Generate and inspect turbulence

Now that we’ve created (and verified) our custom wind speed and turbulence standard deviation functions, we can run our turbulence generation. We first put all of our simulation options into a keyword argument dictionary and then pass those into gen_turb. Note that u_ref is no longer used in the wind speed or sig profiles, but it is needed for the Kaimal spectrum function.

[7]:
kwargs = {'T': T, 'nt': nt, 'u_ref': 10, 'wsp_func': wake_wsp, 'sig_func': wake_sig,
          'cntr_pos': cntr_pos, 'rad': rad, 'u_inf': u_inf, 'sig_inf': 1, 'max_perc': 2}
turb_df = gen_turb(spat_df, **kwargs)

We can inspect the resulting turbulence block to see if it matches our expectations.

Size/elements:

[8]:
turb_df.head()
[8]:
u_p0 u_p1 u_p2 u_p3 u_p4 u_p5 u_p6 u_p7 u_p8 u_p9 ... u_p215 u_p216 u_p217 u_p218 u_p219 u_p220 u_p221 u_p222 u_p223 u_p224
0.0 11.117704 10.991430 11.600350 13.064657 12.041160 12.968533 13.982413 13.478506 14.360823 13.773271 ... 11.465313 12.301751 11.706094 12.460892 13.962985 11.781964 11.318658 11.541540 11.243859 10.625627
1.0 10.760621 10.709210 12.393045 12.464819 11.138296 12.978039 13.840133 14.101283 14.061208 13.741273 ... 11.944170 12.784564 11.745239 12.580565 14.330476 12.272285 11.335118 11.064567 10.018228 9.936472
2.0 10.976096 11.894541 11.903871 12.065677 11.721630 12.919239 13.744259 14.429379 13.846279 13.600350 ... 12.888295 12.819672 11.765050 13.194379 13.667279 12.080016 11.512639 10.860475 9.496326 10.206035
3.0 11.215061 11.370131 12.184096 12.327976 12.443813 12.678066 13.213238 14.051315 13.522231 13.805593 ... 12.754843 12.199719 11.863146 12.921065 13.201568 12.053246 11.551648 10.771590 10.120215 9.962989
4.0 11.999649 11.732922 12.073425 12.553175 12.139090 12.057866 12.945273 13.547350 13.352611 13.315322 ... 12.923019 12.677922 12.138034 12.749256 12.634566 12.146554 12.014980 10.717736 9.789778 10.237484

5 rows × 225 columns

Mean wind speed profile:

[9]:
mean_wsp = turb_df.mean().values
print('Min mwsp: ', mean_wsp.min(), '  Max mwsp: ', mean_wsp.max())

plt.imshow(np.reshape(mean_wsp, (y.size, z.size)).T, interpolation='none',
           origin='lower', extent=[y.min(), y.max(), z.min(), z.max()])
plt.colorbar();
Min mwsp:  11.5   Max mwsp:  12.000000000000002
../_images/notebooks_2_custom_profiles_17_1.png

This looks exactly as we expect it to: the mean wind speed outside the waked area is 12 m/s, and the maximum deficit is 0.5 m/s. Excellent.

Turbulence standard deviation:

[10]:
std_wsp = turb_df.std().values
print('Min std: ', std_wsp.min(), '  Max std: ', std_wsp.max())

plt.imshow(np.reshape(std_wsp, (y.size, z.size)).T, interpolation='none',
           origin='lower', extent=[y.min(), y.max(), z.min(), z.max()])
plt.colorbar();
Min std:  0.726515601661268   Max std:  2.3710445937484366
../_images/notebooks_2_custom_profiles_20_1.png

As you can see, there is more variation in the turbulence profile. This is due to the spatial correlation procedure that occurs during the Veers method simulation; we will never get the exact TI profile we specify. However, we still generally get the correct values: 1.0 m/s in the freestream and 3.0 (200% higher) at the highest.