pyCactus Demo

This notebook demonstrates the core functionality of the pyCactus post-processing module.

Author: Phillip Chiu

In [1]:
# enable autoreload
%load_ext autoreload
%autoreload 2

# set up plotting backend
# %matplotlib nbagg
%matplotlib inline

import matplotlib
import matplotlib.pyplot as plt

# matplotlib.rcParams['figure.figsize'] = (14.0, 10.5)  # large figures
matplotlib.rcParams['figure.figsize'] = (8, 6)         # medium figures
matplotlib.rcParams.update({'font.size': 14})          # label size

# set up paths
import os
import sys

nb_root_dir = os.path.expanduser('~/Notebooks/')      # notebook root dir
nb_dir_name = 'demo1'                                 # notebook work dir name
nb_dir = os.path.abspath(os.path.join(nb_root_dir,
                                      nb_dir_name))   # notebook work dir

# create notebook dir if it doesn't exist
if not os.path.exists(nb_dir):
    os.makedirs(nb_dir)

sys.path.append(nb_dir) # append notebook work dir to pythonpath
os.chdir(nb_dir)        # change path to notebook work dir

# import commonly-used modules
import math
import numpy as np
import pandas as pd
import time as pytime

# apply style (from external notebook)
%run '~/Notebooks/style.ipynb'
clean()
Out[1]:

Add the path to pyCactus and import.

In [2]:
sys.path.append(nb_dir + '/CACTUS-tools/')
import pyCactus

Creating a CactusRun instance

The CactusRun reads and stores information about a single run. The path to the data files is specified when initializing the CactusRun instance. It can be instantiated as follows:

In [3]:
case_name  = 'NREL-5MW'
run_dir    = os.path.join(nb_dir,'NREL-5MW-CACTUS')
In [4]:
tic = pytime.time()
geom_fname = 'NREL-5MW.geom'
run = pyCactus.CactusRun(run_dir, case_name,
                         geom_fname=geom_fname,
                         wakeelem_fnames_pattern='*WakeData_*.csv',
                         field_fnames_pattern='*WakeDefData_*.csv')
print 'Time to instantiante CactusRun class: %2.2f s' % (pytime.time() - tic)
Read input namelist in 0.01 s
Read geometry file in 0.01 s
Read parameter data in 0.00 s
Read revolution-averaged data in 0.00 s
Read blade element data in 0.06 s
Read time data in 0.01 s
Read 20 wake element data headers in 0.00 s
Read 20 wake element data headers in 0.00 s
Read grid dimensions in 0.05 s
Read 6 probe data file headers in 0.00 s

Success: Loaded case `NREL-5MW` from path `/home/phil/Notebooks/demo1/NREL-5MW-CACTUS`

Time to instantiante CactusRun class: 0.17 s

On instantiation, the CactusRun class loads the geometry file along with the time data, blade element data, and revolution-averaged data. It also parses the first line of each of the wake grid and wake node .csv files to determine the output times.

The time, element, and revolution data are stored as Pandas dataframes. In order to reduce both memory usage and the time for class instantiation, data is only read from wake element and field data files (which may be very large) when needed.

Geometry data

The geometry data can be accessed easily by calling the CactusRun.geom attribute, which is an instance of the CactusGeom class.

The geometry data is stored in the globalvars, blades, and struts attributes.

In [5]:
run.geom.globalvars
Out[5]:
{'NBlade': [3],
 'NStrut': [0],
 'RefAR': array([ 3.14159]),
 'RefR': array([ 206.693]),
 'RotN': array([ 1.,  0.,  0.]),
 'RotP': array([ 0.,  0.,  0.]),
 'Type': ' HAWT'}

The blades and struts are lists of variable dictionaries, enumerated by blade number and strut number.

In [6]:
blade_num = 0
run.geom.blades[blade_num].keys()
Out[6]:
['ECtoR',
 'nEz',
 'nEx',
 'nEy',
 'tz',
 'PEy',
 'PEz',
 'ty',
 'QCz',
 'QCy',
 'QCx',
 'iSect',
 'EAreaR',
 'NElem',
 'dr_over_R',
 'r_over_R_elem',
 'FlipN',
 'PEx',
 'CtoR',
 'tx',
 'Blade 1',
 'sEz',
 'sEy',
 'sEx',
 'tEx',
 'tEy',
 'tEz']

Accessing time, revolution-averaged, and blade element data

Time, blade element, and revolution data can be accessed using the appropriate variables names:

  • CactusRun.time_data
  • CactusRun.elem_data
  • CactusRun.rev_data

Each returns a Pandas dataframe whose column names are shown below.

Time data

The column names for the time data are shown below.

In [7]:
run.time_data.columns
Out[7]:
Index([u'Normalized Time (-)', u'Theta (rad)', u'Rev', u'Torque Coeff. (-)',
       u'Power Coeff. (-)', u'Fx Coeff. (-)', u'Fy Coeff. (-)',
       u'Fz Coeff. (-)', u'Blade Fx Coeff. (-)', u'Blade Fy Coeff. (-)',
       u'Blade Fz Coeff. (-)', u'Blade Torque Coeff. (-)',
       u'Blade Fx Coeff. (-).1', u'Blade Fy Coeff. (-).1',
       u'Blade Fz Coeff. (-).1', u'Blade Torque Coeff. (-).1',
       u'Blade Fx Coeff. (-).2', u'Blade Fy Coeff. (-).2',
       u'Blade Fz Coeff. (-).2', u'Blade Torque Coeff. (-).2'],
      dtype='object')

It is easy to plot this data. Here we use the built-in plotting feature of the Pandas dataframe, but this could also be done with NumPy.

In [8]:
plt.figure()
run.time_data.plot(x='Normalized Time (-)',y='Power Coeff. (-)',
                   marker='o')
plt.grid(True)
<matplotlib.figure.Figure at 0x7fe0d02ae0d0>
_images/pyCactus-demo_17_1.png

Revolution-averaged data

The column names for the revolution-averaged data are shown below.

In [9]:
run.rev_data.columns
Out[9]:
Index([u'Rev', u'Power Coeff. (-)', u'Tip Power Coeff. (-)',
       u'Torque Coeff. (-)', u'Fx Coeff. (-)', u'Fy Coeff. (-)',
       u'Fz Coeff. (-)', u'Power (kW)', u'Torque (ft-lbs)'],
      dtype='object')

As with the time data, this can be easily plotted.

In [10]:
plt.figure()
run.rev_data.plot(x='Rev',y='Fx Coeff. (-)',
                   marker='o')
plt.grid(True)
<matplotlib.figure.Figure at 0x7fe0ae2b6b50>
_images/pyCactus-demo_22_1.png

Blade element data

The column names for the blade element data are shown below.

In [11]:
run.bladeelem_data.data.columns
Out[11]:
Index([u'Normalized Time (-)', u'Theta (rad)', u'Blade', u'Element', u'Rev',
       u'x/R (-)', u'y/R (-)', u'z/R (-)', u'AOA25 (deg)', u'AOA50 (deg)',
       u'AOA75 (deg)', u'AdotNorm (-)', u'Re (-)', u'Mach (-)', u'Ur (-)',
       u'IndU (-)', u'IndV (-)', u'IndW (-)', u'GB (?)', u'CL (-)', u'CD (-)',
       u'CM25 (-)', u'CLCirc (-)', u'CN (-)', u'CT (-)', u'Fx (-)', u'Fy (-)',
       u'Fz (-)', u'te (-)'],
      dtype='object')

This data can also be plotted easily. We can extract the blade data at a particular time index by calling CactusRun.blade_data_at_time_index().

For horizontal axis wind turbines, it is often desirable to plot a blade quantity against the distance from the rotation axis. This is demonstrated here using data from the CactusRun.geom instance.

In [12]:
blade_num = 0

# get the blade data at the final time index
time, dfs_blade = run.bladeelem_data.data_at_time_index(time_index=-1)

plt.figure()
plt.plot(run.geom.blades[blade_num]['r_over_R_elem'],
         dfs_blade[blade_num]['CT (-)'])
plt.grid(True)
plt.xlabel('r_over_R_elem')
plt.ylabel('CT (-)')
plt.show()
_images/pyCactus-demo_27_0.png

The blade data can also be time-averaged over specified timesteps. In the following example, we average over the last 5 timesteps.

In [13]:
timesteps = range(-5,0)
CT_averaged = run.bladeelem_data.data_time_average('CT (-)', timesteps, 1)

timesteps
Out[13]:
[-5, -4, -3, -2, -1]
In [14]:
plt.figure()
plt.plot(run.geom.blades[blade_num]['r_over_R_elem'],
         CT_averaged)
plt.grid(True)
plt.xlabel('r_over_R_elem')
plt.ylabel('CT (-)')
plt.show()
_images/pyCactus-demo_30_0.png

Accessing wake element and field data

Due to their potentially large file sizes, wake element and field data are accessed differently than the above data – the entire data file is never loaded into memory.

Wake element data

Viewing times

The times at which we have wake element data can be easily viewed.

In [15]:
run.wakeelems.num_times
Out[15]:
20
In [16]:
run.wakeelems.times
Out[16]:
array([ 0.       ,  0.2080525,  0.416105 ,  0.6241575,  0.83221  ,
        1.040262 ,  1.248315 ,  1.456367 ,  1.66442  ,  1.872472 ,
        2.080525 ,  2.288577 ,  2.49663  ,  2.704682 ,  2.912735 ,
        3.120787 ,  3.32884  ,  3.536892 ,  3.744945 ,  3.952997 ])

Extracting data to NumPy arrays

A dataframe containing data at a particular timestep can be extracted using the method CactusWakeElem.get_df_inst(). This returns a dictionary containing variables names and their values as NumPy arrays.

In [17]:
# Get dataframe for a specific time
ti = 5
wake_df_inst = run.wakeelems.get_df_inst(time=run.wakeelems.times[ti])
wake_df_inst.head()

# extract variables
wakeelemdata, has_node_ids = run.wakeelems.wakedata_from_df(wake_df_inst)
In [18]:
wakeelemdata.keys()
Out[18]:
['node_ids', 'elems', 'u', 'w', 'v', 'y', 'x', 'z']

The second return is a flag which indicates whether the wake element data files have a node_id column. (For compatibility with data generated by versions of CACTUS).

In [19]:
has_node_ids
Out[19]:
True

Writing to VTK

The wake element data can be written to a VTK file to facilitate post-processing with ParaView or other tools. To do this, we can use the CactusWakeElems.write_vtk_series() method. This writes a series of VTK files, and a .pvd collection file which can be read into ParaView directly as a time series.

In [20]:
wakeelem_output_path = os.path.join(nb_dir,'vtk')
run.wakeelems.write_vtk_series(wakeelem_output_path,
                               'wakenode_name',
                               print_status=False)
Out[20]:
(['/home/phil/Notebooks/demo1/vtk/wakenode_name_0.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_1.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_2.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_3.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_4.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_5.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_6.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_7.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_8.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_9.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_10.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_11.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_12.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_13.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_14.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_15.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_16.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_17.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_18.vtu',
  '/home/phil/Notebooks/demo1/vtk/wakenode_name_19.vtu'],
 '/home/phil/Notebooks/demo1/vtk/wakenode_name.pvd')

Field data

Viewing times

In [21]:
run.wakeelems.num_times
Out[21]:
20
In [22]:
run.field.times
Out[22]:
array([ 0.       ,  0.2080525,  0.416105 ,  0.6241575,  0.83221  ,
        1.040262 ,  1.248315 ,  1.456367 ,  1.66442  ,  1.872472 ,
        2.080525 ,  2.288577 ,  2.49663  ,  2.704682 ,  2.912735 ,
        3.120787 ,  3.32884  ,  3.536892 ,  3.744945 ,  3.952997 ])

Extracting data to NumPy arrays

A dataframe containing data at a particular timestep can be extracted using the method CactusField.get_df_inst(). This returns a dictionary containing variables names and their values as NumPy arrays.

In [23]:
# Get dataframe for a specific time
ti = 5
field_df_inst = run.field.get_df_inst(time=run.wakeelems.times[ti])
field_df_inst.head()

# extract variables
fielddata, fielddims = run.field.fielddata_from_df(field_df_inst)
In [24]:
fielddata.keys()
Out[24]:
['Ufs', 'Wfs', 'U', 'W', 'V', 'Y', 'X', 'Z', 'Vfs']

The second return is a dictionary of the field dimensions.

In [25]:
fielddims
Out[25]:
{'dx': 0.0,
 'dy': 0.040000000000000001,
 'dz': 0.040000000000000001,
 'nx': 1,
 'ny': 100,
 'nz': 100,
 'xlim': [0.0, 0.0],
 'ylim': [-2.0, 2.0],
 'zlim': [-2.0, 2.0]}

The dimensions of the returned arrays match the grid dimensions.

In [26]:
fielddata['U'].shape
Out[26]:
(100, 100, 1)

Time series at a specific location on the Cartesian grid

Data at a specific location can be extracted. This operation may take some time, since it loops through all the data files.

In [27]:
# enter a list of point coordinates
point_list = [(0.0, 0.5, 0.5),
              (1.0, 0.0, 0.0)]

tic = pytime.time()
timeseries_dict, nearest_points = run.field.pointdata_time_series(point_list, ti_start=0, ti_end=-1)
print 'Time to produce series: %2.2f s' % (pytime.time() - tic)

print nearest_points
Time to produce series: 0.70 s
[(0.0, 0.50505048, 0.50505048), (0.0, -0.02020202, -0.02020202)]
In [28]:
# extract the data
t   = timeseries_dict['t']
u   = timeseries_dict['u']
v   = timeseries_dict['v']
w   = timeseries_dict['w']
ufs = timeseries_dict['ufs']
vfs = timeseries_dict['vfs']
wfs = timeseries_dict['wfs']
In [29]:
point_id = 1

print t
# plot the data at the point_id'th point
plt.figure()
plt.plot(t, u[point_id,:], label='$u$', lw=2)
plt.plot(t, v[point_id,:], label='$v$', lw=2)
plt.plot(t, w[point_id,:], label='$w$', lw=2)

plt.title('Time series')
plt.xlabel('non-dimensionalized time, $t^*$')
plt.ylabel('non-dimensionalized velocity')
plt.legend(loc='best')

plt.grid(True)
plt.tight_layout()
plt.show()
[ 0.         0.2080525  0.416105   0.6241575  0.83221    1.040262   1.248315
  1.456367   1.66442    1.872472   2.080525   2.288577   2.49663    2.704682
  2.912735   3.120787   3.32884    3.536892   3.744945 ]
_images/pyCactus-demo_59_1.png

Field averaging

We can time average the field data using the CactusField.field_time_average() method. This returns a dictionary containing the averaged data.

In [30]:
tic = pytime.time()

averaged_data = run.field.field_time_average(ti_start=-5,
                                             ti_end=-1)

print 'Time to average fields: %2.2f s' % (pytime.time() - tic)
Time to average fields: 0.19 s
In [31]:
print averaged_data.keys()
['Ufs', 'U', 't', 'W', 'V', 'Y', 'X', 'Z', 'Vfs', 'Wfs']

The t field gives the normalized times which were averaged.

In [32]:
print averaged_data['t']
[ 3.120787  3.32884   3.536892  3.744945]

The averaged data may be plotted.

In [33]:
import matplotlib.cm as cm

# Get the data
X   = averaged_data['X']
Y   = averaged_data['Y']
Z   = averaged_data['Z']
U   = averaged_data['U']
V   = averaged_data['V']
W   = averaged_data['W']
Ufs = averaged_data['Ufs']
Vfs = averaged_data['Vfs']
Wfs = averaged_data['Wfs']

# Reshape to 2D
ny = run.field.grid_dims['ny']
nz = run.field.grid_dims['nz']

X   = np.reshape(X, [ny, nz])
Y   = np.reshape(Y, [ny, nz])
Z   = np.reshape(Z, [ny, nz])
U   = np.reshape(U, [ny, nz])
V   = np.reshape(V, [ny, nz])
W   = np.reshape(W, [ny, nz])
Ufs = np.reshape(Ufs, [ny, nz])
Vfs = np.reshape(Vfs, [ny, nz])
Wfs = np.reshape(Wfs, [ny, nz])

# u
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.contourf(Y,Z,U+Ufs,128,cmap=cm.coolwarm)
fig.colorbar(cax)
ax.axis('image')
ax.set_title('$U/U_{\infty}$, time-averaged')
ax.set_xlabel('$x/R$')
ax.set_ylabel('$y/R$')

plt.show()
_images/pyCactus-demo_66_0.png

Writing to VTK file

As with the wake element data, the field data can also be written to VTK files, using the CactusField.write_vtk_series() method. This writes a series of structured VTK files (.vts), and a .pvd collection file which can be read into ParaView directly as a time series.

In [34]:
field_output_path    = os.path.join(nb_dir,'vtk')
run.field.write_vtk_series(field_output_path,
                           'field',
                           print_status=False)
Out[34]:
(['/home/phil/Notebooks/demo1/vtk/field_0.vts',
  '/home/phil/Notebooks/demo1/vtk/field_1.vts',
  '/home/phil/Notebooks/demo1/vtk/field_2.vts',
  '/home/phil/Notebooks/demo1/vtk/field_3.vts',
  '/home/phil/Notebooks/demo1/vtk/field_4.vts',
  '/home/phil/Notebooks/demo1/vtk/field_5.vts',
  '/home/phil/Notebooks/demo1/vtk/field_6.vts',
  '/home/phil/Notebooks/demo1/vtk/field_7.vts',
  '/home/phil/Notebooks/demo1/vtk/field_8.vts',
  '/home/phil/Notebooks/demo1/vtk/field_9.vts',
  '/home/phil/Notebooks/demo1/vtk/field_10.vts',
  '/home/phil/Notebooks/demo1/vtk/field_11.vts',
  '/home/phil/Notebooks/demo1/vtk/field_12.vts',
  '/home/phil/Notebooks/demo1/vtk/field_13.vts',
  '/home/phil/Notebooks/demo1/vtk/field_14.vts',
  '/home/phil/Notebooks/demo1/vtk/field_15.vts',
  '/home/phil/Notebooks/demo1/vtk/field_16.vts',
  '/home/phil/Notebooks/demo1/vtk/field_17.vts',
  '/home/phil/Notebooks/demo1/vtk/field_18.vts',
  '/home/phil/Notebooks/demo1/vtk/field_19.vts'],
 '/home/phil/Notebooks/demo1/vtk/field.pvd')

Probe data

The CactusRun.probe attribute is an instance of the CactusProbe class. We can check the number of probes and their locations.

In [35]:
run.probes.num_probes
Out[35]:
6
In [36]:
run.probes.locations
Out[36]:
{0: (0.0, 0.0, 0.0),
 1: (1.0, 0.0, 0.0),
 2: (2.0, 0.0, 0.0),
 3: (3.0, 0.0, 0.0),
 4: (4.0, 0.0, 0.0),
 5: (5.0, 0.0, 0.0)}

We can also extract the probe data using get_probe_data_by_id().

In [37]:
run.probes.get_probe_data_by_id(1).head()
Out[37]:
Normalized Time (-) U/Uinf (-) V/Uinf (-) W/Uinf (-) Ufs/Uinf (-) Vfs/Uinf (-) Wfs/Uinf (-)
0 0.000000 0.000288 3.685064e-09 -2.268072e-09 1 0 0
1 0.041611 -0.004127 -1.330602e-09 1.217035e-09 1 0 0
2 0.083221 -0.007950 1.282547e-09 5.536929e-10 1 0 0
3 0.124832 -0.012012 4.207945e-10 3.674458e-10 1 0 0
4 0.166442 -0.016445 6.160830e-10 5.428379e-10 1 0 0

The velocity for any given probe can be plotted quickly by calling plot_probe_data_by_id().

In [38]:
fig, ax = run.probes.plot_probe_data_by_id(0,timestep=False)
_images/pyCactus-demo_76_0.png

Accessing the input namelist

The input file namelist can be accessed using the namelist instance attribute.

In [39]:
run.input.namelist
Out[39]:
OrderedDict([('configinputs',
              OrderedDict([('gpflag', 0),
                           ('nr', 5),
                           ('nti', 20),
                           ('iut', 1),
                           ('convrg', -1),
                           ('ifc', 0),
                           ('nric', 9),
                           ('ntif', 30),
                           ('iutf', 1),
                           ('convrgf', 0.0001),
                           ('ixterm', 0),
                           ('xstop', 5)])),
             ('caseinputs',
              OrderedDict([('jbtitle', 'NREL 5 MW'),
                           ('rho', 0.002378),
                           ('vis', 3.739e-07),
                           ('tempr', 60.0),
                           ('slex', 0.0),
                           ('hblref', 0.0),
                           ('hag', 295.276),
                           ('rpm', 12.1),
                           ('ut', 7.55),
                           ('geomfilepath', './NREL-5MW.geom'),
                           ('nsect', 8),
                           ('afdpath',
                            ['./airfoils/Cylinder1.dat',
                             './airfoils/Cylinder2.dat',
                             './airfoils/DU40_A17.dat',
                             './airfoils/DU35_A17.dat',
                             './airfoils/DU30_A17.dat',
                             './airfoils/DU25_A17.dat',
                             './airfoils/DU21_A17.dat',
                             './airfoils/NACA64_A17.dat'])])),
             ('configoutputs',
              OrderedDict([('diagoutflag', 1),
                           ('output_elflag', 1),
                           ('wakeelementoutflag', 1),
                           ('wakegridoutflag', 1),
                           ('probeflag', 1),
                           ('probespecpath', './probes.dat')]))])

This namelist can easily be modified and rewritten as a separate file (perhaps facilitating parametric studies of parameters).

In [40]:
run.input.namelist['configinputs']['nti'] = 30
In [41]:
import StringIO

output = StringIO.StringIO()
run.input.namelist.write(output)
In [42]:
print output.getvalue()
&configinputs
    gpflag = 0
    nr = 5
    nti = 30
    iut = 1
    convrg = -1
    ifc = 0
    nric = 9
    ntif = 30
    iutf = 1
    convrgf = 0.0001
    ixterm = 0
    xstop = 5
/

&caseinputs
    jbtitle = 'NREL 5 MW'
    rho = 0.002378
    vis = 3.739e-07
    tempr = 60.0
    slex = 0.0
    hblref = 0.0
    hag = 295.276
    rpm = 12.1
    ut = 7.55
    geomfilepath = './NREL-5MW.geom'
    nsect = 8
    afdpath = './airfoils/Cylinder1.dat', './airfoils/Cylinder2.dat', './airfoils/DU40_A17.dat',
              './airfoils/DU35_A17.dat', './airfoils/DU30_A17.dat', './airfoils/DU25_A17.dat',
              './airfoils/DU21_A17.dat', './airfoils/NACA64_A17.dat'
/

&configoutputs
    diagoutflag = 1
    output_elflag = 1
    wakeelementoutflag = 1
    wakegridoutflag = 1
    probeflag = 1
    probespecpath = './probes.dat'
/