Run-time postprocessing
For pump performance calculation (and in many other cases as well), one usually needs data about pressure rise (head), flow, and power. If cavitation is an issue, maximum and minimum values for pressure come in handy, also maximum and minimum velocity can tell about convergence or numerical issues. All of these values are best calculated at runtime so it’s best to put the settings in controlDict.functionObjects.
// impeller forces for torque/power/viscous losses/axial and radial forces/mechanical losses
forces_impeller
{
type forces;
libs ("libforces.so");
patches (impeller_walls);
rho rhoInf;
rhoInf 985; // depends on temperature, also check viscosity in constant/transportProperties
CofR (0 0 0);
}
// pressure difference between inlet and outlet for head calculation
delta_p
{
type fieldValueDelta;
libs ("libfieldFunctionObjects.so");
operation subtract;
region1
{
type surfaceFieldValue;
operation average;
fields (p);
writeFields no;
regionType patch;
name outlet;
}
region2
{
type surfaceFieldValue;
operation average;
fields (p);
writeFields no;
regionType patch;
name inlet;
}
}
// flow (is specified in 0/U but here it's added for an additional check)
Q
{
type surfaceFieldValue;
libs ("libfieldFunctionObjects.so");
writeFields false;
surfaceFormat foam;
regionType patch;
name outlet;
operation areaIntegrate;
fields
(
U
);
}
// maximum values: maximum velocity should be around impeller circumferential velocity
// difference between minimum pressure and inlet pressure is NPSH
maxValues
{
type fieldMinMax;
libs ("libfieldFunctionObjects.so");
writeToFile no;
log yes;
location no;
mode magnitude;
fields (U p);
}
Values are calculated after every time step and results are saved in postProcessing/ directory.
Post-postprocessing
There is also a handful of other quantities of interest like yPlus for mesh quality check and Q and vorticity for turbulence/losses visualisation. The problem with these quantities are that the whole field gets saved each timestep. If your writeInterval in controlDict is not 1 OpenFOAM makes a mess with timestep directories. Now every timestep has a directory, some only have yPlus and Q fields and others have all calculated quantities. This makes working with Paraview difficult so it’s better to postprocess these fields separately.
mpirun -np 4 pimpleFoam -parallel -postProcess -latestTime -func yPlus mpirun -np 4 pimpleFoam -parallel -postProcess -latestTime -func Q mpirun -np 4 pimpleFoam -parallel -postProcess -latestTime -func vorticity
The postProcessing/ directory
If for any reason the simulation is interrupted and then restarted from latestTime (check your controlDict), new files are added to postProcessing directory. Every added file gets an additional timestep timestamp added to its name. With many restarts the files multiply and make gathering data difficult.
Gathering data
This is a script for reading postprocessing data from all created files mentioned above. It gathers the values in a single Python dictionary and pickles it to a file. It can then be read in Paraview with programmable filter/source for visualization or read from another Python script for plotting and so on.
from subprocess import Popen, PIPE
import os
import re
import pickle
# case times
times = []
# data to be written
data = {
# [time]: {
# delta_p: [number],
# Q: [number]
# forces_impeller: [F_pressure, F_viscous, M_pressure, M_viscous]
# }
}
for t in times:
data[t] = {}
def read_pp_data(pp_object, parse_function):
for froot, _, fnames in os.walk(os.path.join('postProcessing', pp_object)):
for fname in fnames:
filename = os.path.join(froot, fname)
if not os.path.isfile(filename):
continue
with open(filename, 'r') as f:
print filename
# skip lines with comment
for line in f:
if line[0] == '#':
continue
# timestep
time = line.split()[0]
# add to an existing entry or create a new one
if time not in times:
times.append(time)
data[time] = {}
# add data to collection
data[time][pp_object] = parse_function(line)
# parsing files: each value has different format
split_re = re.compile('[ \t\n\r()]+') # split: whitespace, parenthesis
def parse_delta_p(line): # delta_p function object
values = line.split()
return float(values[1])
def parse_Q(line): # surfaceFieldValue areaIntegrate (flow)
return float(split_re.split(line)[2])
def parse_forces_impeller(line): # forces functionObject
# forces format:
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# Time forces(pressure viscous porous) moments(pressure viscous porous)
forces = split_re.split(line)
# interested in: pressure and viscous forces and moments
indexes = (1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15)
return [float(forces[i]) for i in indexes]
read_pp_data('delta_p', parse_delta_p)
read_pp_data('Q', parse_Q)
read_pp_data('forces_impeller', parse_forces_impeller)
# change text numbers to floats
for k in data.keys():
data[float(k)] = data[k]
del data[k]
# save the data to be read in paraView (?)
with open('postProcessing.pickle', 'wb') as f:
pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
Plotting collected data
Loading a pickled dictionary is a piece of cake. I usually cut the first few results away since timesteps usually don’t converge and therefore values are unphysical.
import pickle
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats
import collect_pp # import the above script to avoid running it manually
# start only after transients decay
t_start = 0.001 # here interesting data starts
# pickled data format:
# 0.000444: {
# 'Q': -0.000222930392,
# 'delta_p': 200.0,
# 'forces_impeller': [-70.6219451, -0.307142376, 0.116242584, 0.0130158996, -0.0167682449, 0.00424048528, -0.320480923, -0.00105645189, -0.00185268198, -0.0417415834, -4.51203576e-05, -0.00011419315]
# },
with open('postProcessing.pickle', 'rb') as f:
data = pickle.load(f)
# times
times = data.keys()
times.sort()
n_start = np.searchsorted(np.array(times), t_start)
times = times[n_start:]
p_H = [] # head
p_Q = [] # flow
p_Mp = [] # pressure torque
p_Mv = [] # viscous torque
p_Fx = [] # axial force
for t in times:
try:
p_H.append(data[t]['delta_p'])
p_Q.append(-data[t]['Q'])
Mp.append(-data[t]['forces_impeller'][6])
Mv.append(-data[t]['forces_impeller'][9])
Fx.append(data[t]['forces_impeller'][0])
# time
p_times.append(t)
except KeyError:
continue
Filtering stuff
Ocassionally a timestep doesn’t converge and so calculated forces/moments/pressures are way too high or too low. That ruins the day for graphing and these values should be filtered. Actually, it would be better to find out why calculation doesn’t converge in the first place but I haven’t had the time yet for a proper ‘debugging’ session. So I use a little trim utility from scipy:
# filter out the outliers; weird unexplained results that occur randomly
# first, determine which are they.
def filter_data(values):
# use trimmed values for average or it doesn't 'mean' anything
avg = np.average(scipy.stats.trim_mean(values, 0.1))
return np.array(np.where(np.abs(values - avg) > abs(0.5*avg)))[0]
i_filter = np.concatenate((
filter_data(p_H),
filter_data(p_Q),
filter_data(p_Mp),
filter_data(p_Mv),
filter_data(p_Fx)))
i_filter = list(set(i_filter))
# delete all data with given indexes
p_times = np.delete(p_times, i_filter)
p_H = np.delete(p_H, i_filter)
p_Q = np.delete(p_Q, i_filter)
p_Mp = np.delete(p_Mp, i_filter)
p_Mv = np.delete(p_Mv, i_filter)
p_Fx = np.delete(p_Fx, i_filter)
Now you’re ready to do whatever with the data. Like plotting:
# plot results
fig, ax1 = plt.subplots()
ax1.plot(p_times, p_H, 'b-')
ax1.set_xlabel('Time (s)')
ax1.set_ylabel('H [dm]', color='b')
ax1.tick_params('y', colors='b')
ax2 = ax1.twinx()
ax2.plot(p_times, p_Mp, 'r.')
ax2.set_ylabel('M [Nm]', color='r')
ax2.tick_params('y', colors='r')
Enjoy!




