parallelize addition of datasets

threads does not work, muliprocessing shows good performance: Overhead
is small compared to the performance gain.
Especially useful for long running functions of the orientation class
This commit is contained in:
Martin Diehl 2020-02-21 19:24:26 +01:00
parent b9966b95e0
commit 05476ff85c
2 changed files with 307 additions and 379 deletions

View File

@ -1,7 +1,8 @@
from queue import Queue
import multiprocessing
import re
import glob
import os
from functools import partial
import vtk
from vtk.util import numpy_support
@ -443,6 +444,17 @@ class DADF5():
return f['geometry/x_c'][()]
@staticmethod
def _add_absolute(x):
return {
'data': np.abs(x['data']),
'label': '|{}|'.format(x['label']),
'meta': {
'Unit': x['meta']['Unit'],
'Description': 'Absolute value of {} ({})'.format(x['label'],x['meta']['Description']),
'Creator': 'dadf5.py:add_abs v{}'.format(version)
}
}
def add_absolute(self,x):
"""
Add absolute value.
@ -453,21 +465,24 @@ class DADF5():
Label of scalar, vector, or tensor dataset to take absolute value of.
"""
def _add_absolute(x):
self.__add_generic_pointwise(self._add_absolute,{'x':x})
@staticmethod
def _add_calculation(**kwargs):
formula = kwargs['formula']
for d in re.findall(r'#(.*?)#',formula):
formula = formula.replace('#{}#'.format(d),"kwargs['{}']['data']".format(d))
return {
'data': np.abs(x['data']),
'label': '|{}|'.format(x['label']),
'data': eval(formula),
'label': kwargs['label'],
'meta': {
'Unit': x['meta']['Unit'],
'Description': 'Absolute value of {} ({})'.format(x['label'],x['meta']['Description']),
'Creator': 'dadf5.py:add_abs v{}'.format(version)
'Unit': kwargs['unit'],
'Description': '{} (formula: {})'.format(kwargs['description'],kwargs['formula']),
'Creator': 'dadf5.py:add_calculation v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_absolute,{'x':x})
def add_calculation(self,label,formula,unit='n/a',description=None,vectorized=True):
"""
Add result of a general formula.
@ -489,28 +504,24 @@ class DADF5():
if not vectorized:
raise NotImplementedError
def _add_calculation(**kwargs):
formula = kwargs['formula']
for d in re.findall(r'#(.*?)#',formula):
formula = formula.replace('#{}#'.format(d),"kwargs['{}']['data']".format(d))
return {
'data': eval(formula),
'label': kwargs['label'],
'meta': {
'Unit': kwargs['unit'],
'Description': '{} (formula: {})'.format(kwargs['description'],kwargs['formula']),
'Creator': 'dadf5.py:add_calculation v{}'.format(version)
}
}
dataset_mapping = {d:d for d in set(re.findall(r'#(.*?)#',formula))} # datasets used in the formula
args = {'formula':formula,'label':label,'unit':unit,'description':description}
self.__add_generic_pointwise(_add_calculation,dataset_mapping,args)
self.__add_generic_pointwise(self._add_calculation,dataset_mapping,args)
@staticmethod
def _add_Cauchy(P,F):
return {
'data': mechanics.Cauchy(P['data'],F['data']),
'label': 'sigma',
'meta': {
'Unit': P['meta']['Unit'],
'Description': 'Cauchy stress calculated from {} ({}) '.format(P['label'],
P['meta']['Description'])+\
'and {} ({})'.format(F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_Cauchy v{}'.format(version)
}
}
def add_Cauchy(self,P='P',F='F'):
"""
Add Cauchy stress calculated from first Piola-Kirchhoff stress and deformation gradient.
@ -523,23 +534,20 @@ class DADF5():
Label of the dataset containing the deformation gradient. Defaults to F.
"""
def _add_Cauchy(P,F):
self.__add_generic_pointwise(self._add_Cauchy,{'P':P,'F':F})
@staticmethod
def _add_determinant(T):
return {
'data': mechanics.Cauchy(P['data'],F['data']),
'label': 'sigma',
'data': np.linalg.det(T['data']),
'label': 'det({})'.format(T['label']),
'meta': {
'Unit': P['meta']['Unit'],
'Description': 'Cauchy stress calculated from {} ({}) '.format(P['label'],
P['meta']['Description'])+\
'and {} ({})'.format(F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_Cauchy v{}'.format(version)
'Unit': T['meta']['Unit'],
'Description': 'Determinant of tensor {} ({})'.format(T['label'],T['meta']['Description']),
'Creator': 'dadf5.py:add_determinant v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_Cauchy,{'P':P,'F':F})
def add_determinant(self,T):
"""
Add the determinant of a tensor.
@ -550,33 +558,11 @@ class DADF5():
Label of tensor dataset.
"""
def _add_determinant(T):
return {
'data': np.linalg.det(T['data']),
'label': 'det({})'.format(T['label']),
'meta': {
'Unit': T['meta']['Unit'],
'Description': 'Determinant of tensor {} ({})'.format(T['label'],T['meta']['Description']),
'Creator': 'dadf5.py:add_determinant v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_determinant,{'T':T})
self.__add_generic_pointwise(self._add_determinant,{'T':T})
def add_deviator(self,T):
"""
Add the deviatoric part of a tensor.
Parameters
----------
T : str
Label of tensor dataset.
"""
@staticmethod
def _add_deviator(T):
if not np.all(np.array(T['data'].shape[1:]) == np.array([3,3])):
raise ValueError
@ -589,10 +575,30 @@ class DADF5():
'Creator': 'dadf5.py:add_deviator v{}'.format(version)
}
}
def add_deviator(self,T):
"""
Add the deviatoric part of a tensor.
self.__add_generic_pointwise(_add_deviator,{'T':T})
Parameters
----------
T : str
Label of tensor dataset.
"""
self.__add_generic_pointwise(self._add_deviator,{'T':T})
@staticmethod
def _add_eigenvalue(S):
return {
'data': mechanics.eigenvalues(S['data']),
'label': 'lambda({})'.format(S['label']),
'meta' : {
'Unit': S['meta']['Unit'],
'Description': 'Eigenvalues of {} ({})'.format(S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_eigenvalues v{}'.format(version)
}
}
def add_eigenvalues(self,S):
"""
Add eigenvalues of symmetric tensor.
@ -603,21 +609,20 @@ class DADF5():
Label of symmetric tensor dataset.
"""
def _add_eigenvalue(S):
self.__add_generic_pointwise(self._add_eigenvalue,{'S':S})
@staticmethod
def _add_eigenvector(S):
return {
'data': mechanics.eigenvalues(S['data']),
'label': 'lambda({})'.format(S['label']),
'data': mechanics.eigenvectors(S['data']),
'label': 'v({})'.format(S['label']),
'meta' : {
'Unit': S['meta']['Unit'],
'Description': 'Eigenvalues of {} ({})'.format(S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_eigenvalues v{}'.format(version)
'Unit': '1',
'Description': 'Eigenvectors of {} ({})'.format(S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_eigenvectors v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_eigenvalue,{'S':S})
def add_eigenvectors(self,S):
"""
Add eigenvectors of symmetric tensor.
@ -628,35 +633,11 @@ class DADF5():
Label of symmetric tensor dataset.
"""
def _add_eigenvector(S):
return {
'data': mechanics.eigenvectors(S['data']),
'label': 'v({})'.format(S['label']),
'meta' : {
'Unit': '1',
'Description': 'Eigenvectors of {} ({})'.format(S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_eigenvectors v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_eigenvector,{'S':S})
self.__add_generic_pointwise(self._add_eigenvector,{'S':S})
def add_IPFcolor(self,q,l):
"""
Add RGB color tuple of inverse pole figure (IPF) color.
Parameters
----------
q : str
Label of the dataset containing the crystallographic orientation as quaternions.
l : numpy.array of shape (3)
Lab frame direction for inverse pole figure.
"""
@staticmethod
def _add_IPFcolor(q,l):
d = np.array(l)
d_unit = d/np.linalg.norm(d)
m = util.scale_to_coprime(d)
@ -678,10 +659,32 @@ class DADF5():
'Creator': 'dadf5.py:add_IPFcolor v{}'.format(version)
}
}
def add_IPFcolor(self,q,l):
"""
Add RGB color tuple of inverse pole figure (IPF) color.
self.__add_generic_pointwise(_add_IPFcolor,{'q':q},{'l':l})
Parameters
----------
q : str
Label of the dataset containing the crystallographic orientation as quaternions.
l : numpy.array of shape (3)
Lab frame direction for inverse pole figure.
"""
self.__add_generic_pointwise(self._add_IPFcolor,{'q':q},{'l':l})
@staticmethod
def _add_maximum_shear(S):
return {
'data': mechanics.maximum_shear(S['data']),
'label': 'max_shear({})'.format(S['label']),
'meta': {
'Unit': S['meta']['Unit'],
'Description': 'Maximum shear component of {} ({})'.format(S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_maximum_shear v{}'.format(version)
}
}
def add_maximum_shear(self,S):
"""
Add maximum shear components of symmetric tensor.
@ -692,21 +695,23 @@ class DADF5():
Label of symmetric tensor dataset.
"""
def _add_maximum_shear(S):
self.__add_generic_pointwise(self._add_maximum_shear,{'S':S})
@staticmethod
def _add_Mises(S):
t = 'strain' if S['meta']['Unit'] == '1' else \
'stress'
return {
'data': mechanics.maximum_shear(S['data']),
'label': 'max_shear({})'.format(S['label']),
'data': mechanics.Mises_strain(S['data']) if t=='strain' else mechanics.Mises_stress(S['data']),
'label': '{}_vM'.format(S['label']),
'meta': {
'Unit': S['meta']['Unit'],
'Description': 'Maximum shear component of {} ({})'.format(S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_maximum_shear v{}'.format(version)
'Description': 'Mises equivalent {} of {} ({})'.format(t,S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_Mises v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_maximum_shear,{'S':S})
def add_Mises(self,S):
"""
Add the equivalent Mises stress or strain of a symmetric tensor.
@ -717,37 +722,11 @@ class DADF5():
Label of symmetric tensorial stress or strain dataset.
"""
def _add_Mises(S):
t = 'strain' if S['meta']['Unit'] == '1' else \
'stress'
return {
'data': mechanics.Mises_strain(S['data']) if t=='strain' else mechanics.Mises_stress(S['data']),
'label': '{}_vM'.format(S['label']),
'meta': {
'Unit': S['meta']['Unit'],
'Description': 'Mises equivalent {} of {} ({})'.format(t,S['label'],S['meta']['Description']),
'Creator': 'dadf5.py:add_Mises v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_Mises,{'S':S})
self.__add_generic_pointwise(self._add_Mises,{'S':S})
def add_norm(self,x,ord=None):
"""
Add the norm of vector or tensor.
Parameters
----------
x : str
Label of vector or tensor dataset.
ord : {non-zero int, inf, -inf, fro, nuc}, optional
Order of the norm. inf means NumPys inf object. For details refer to numpy.linalg.norm.
"""
@staticmethod
def _add_norm(x,ord):
o = ord
if len(x['data'].shape) == 2:
axis = 1
@ -769,24 +748,23 @@ class DADF5():
'Creator': 'dadf5.py:add_norm v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_norm,{'x':x},{'ord':ord})
def add_PK2(self,P='P',F='F'):
def add_norm(self,x,ord=None):
"""
Add 2. Piola-Kirchhoff calculated from first Piola-Kirchhoff stress and deformation gradient.
Add the norm of vector or tensor.
Parameters
----------
P : str, optional
Label first Piola-Kirchhoff stress dataset. Defaults to P.
F : str, optional
Label of deformation gradient dataset. Defaults to F.
x : str
Label of vector or tensor dataset.
ord : {non-zero int, inf, -inf, fro, nuc}, optional
Order of the norm. inf means NumPys inf object. For details refer to numpy.linalg.norm.
"""
def _add_PK2(P,F):
self.__add_generic_pointwise(self._add_norm,{'x':x},{'ord':ord})
@staticmethod
def _add_PK2(P,F):
return {
'data': mechanics.PK2(P['data'],F['data']),
'label': 'S',
@ -798,26 +776,23 @@ class DADF5():
'Creator': 'dadf5.py:add_PK2 v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_PK2,{'P':P,'F':F})
def add_pole(self,q,p,polar=False):
def add_PK2(self,P='P',F='F'):
"""
Add coordinates of stereographic projection of given pole in crystal frame.
Add second Piola-Kirchhoff calculated from first Piola-Kirchhoff stress and deformation gradient.
Parameters
----------
q : str
Label of the dataset containing the crystallographic orientation as quaternions.
p : numpy.array of shape (3)
Crystallographic direction or plane.
polar : bool, optional
Give pole in polar coordinates. Defaults to False.
P : str, optional
Label first Piola-Kirchhoff stress dataset. Defaults to P.
F : str, optional
Label of deformation gradient dataset. Defaults to F.
"""
def _add_pole(q,p,polar):
self.__add_generic_pointwise(self._add_PK2,{'P':P,'F':F})
@staticmethod
def _add_pole(q,p,polar):
pole = np.array(p)
unit_pole = pole/np.linalg.norm(pole)
m = util.scale_to_coprime(pole)
@ -839,10 +814,36 @@ class DADF5():
'Creator' : 'dadf5.py:add_pole v{}'.format(version)
}
}
def add_pole(self,q,p,polar=False):
"""
Add coordinates of stereographic projection of given pole in crystal frame.
self.__add_generic_pointwise(_add_pole,{'q':q},{'p':p,'polar':polar})
Parameters
----------
q : str
Label of the dataset containing the crystallographic orientation as quaternions.
p : numpy.array of shape (3)
Crystallographic direction or plane.
polar : bool, optional
Give pole in polar coordinates. Defaults to False.
"""
self.__add_generic_pointwise(self._add_pole,{'q':q},{'p':p,'polar':polar})
@staticmethod
def _add_rotational_part(F):
if not np.all(np.array(F['data'].shape[1:]) == np.array([3,3])):
raise ValueError
return {
'data': mechanics.rotational_part(F['data']),
'label': 'R({})'.format(F['label']),
'meta': {
'Unit': F['meta']['Unit'],
'Description': 'Rotational part of {} ({})'.format(F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_rotational_part v{}'.format(version)
}
}
def add_rotational_part(self,F):
"""
Add rotational part of a deformation gradient.
@ -853,33 +854,12 @@ class DADF5():
Label of deformation gradient dataset.
"""
def _add_rotational_part(F):
return {
'data': mechanics.rotational_part(F['data']),
'label': 'R({})'.format(F['label']),
'meta': {
'Unit': F['meta']['Unit'],
'Description': 'Rotational part of {} ({})'.format(F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_rotational_part v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_rotational_part,{'F':F})
self.__add_generic_pointwise(self._add_rotational_part,{'F':F})
def add_spherical(self,T):
"""
Add the spherical (hydrostatic) part of a tensor.
Parameters
----------
T : str
Label of tensor dataset.
"""
@staticmethod
def _add_spherical(T):
if not np.all(np.array(T['data'].shape[1:]) == np.array([3,3])):
raise ValueError
@ -892,10 +872,32 @@ class DADF5():
'Creator': 'dadf5.py:add_spherical v{}'.format(version)
}
}
def add_spherical(self,T):
"""
Add the spherical (hydrostatic) part of a tensor.
self.__add_generic_pointwise(_add_spherical,{'T':T})
Parameters
----------
T : str
Label of tensor dataset.
"""
self.__add_generic_pointwise(self._add_spherical,{'T':T})
@staticmethod
def _add_strain_tensor(F,t,m):
if not np.all(np.array(F['data'].shape[1:]) == np.array([3,3])):
raise ValueError
return {
'data': mechanics.strain_tensor(F['data'],t,m),
'label': 'epsilon_{}^{}({})'.format(t,m,F['label']),
'meta': {
'Unit': F['meta']['Unit'],
'Description': 'Strain tensor of {} ({})'.format(F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_strain_tensor v{}'.format(version)
}
}
def add_strain_tensor(self,F='F',t='V',m=0.0):
"""
Add strain tensor of a deformation gradient.
@ -913,21 +915,24 @@ class DADF5():
Order of the strain calculation. Defaults to 0.0.
"""
def _add_strain_tensor(F,t,m):
self.__add_generic_pointwise(self._add_strain_tensor,{'F':F},{'t':t,'m':m})
@staticmethod
def _add_stretch_tensor(F,t):
if not np.all(np.array(F['data'].shape[1:]) == np.array([3,3])):
raise ValueError
return {
'data': mechanics.strain_tensor(F['data'],t,m),
'label': 'epsilon_{}^{}({})'.format(t,m,F['label']),
'data': mechanics.left_stretch(F['data']) if t == 'V' else mechanics.right_stretch(F['data']),
'label': '{}({})'.format(t,F['label']),
'meta': {
'Unit': F['meta']['Unit'],
'Description': 'Strain tensor of {} ({})'.format(F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_strain_tensor v{}'.format(version)
'Description': '{} stretch tensor of {} ({})'.format('Left' if t == 'V' else 'Right',
F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_stretch_tensor v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_strain_tensor,{'F':F},{'t':t,'m':m})
def add_stretch_tensor(self,F='F',t='V'):
"""
Add stretch tensor of a deformation gradient.
@ -941,77 +946,54 @@ class DADF5():
Defaults to V.
"""
def _add_stretch_tensor(F,t):
return {
'data': mechanics.left_stretch(F['data']) if t == 'V' else mechanics.right_stretch(F['data']),
'label': '{}({})'.format(t,F['label']),
'meta': {
'Unit': F['meta']['Unit'],
'Description': '{} stretch tensor of {} ({})'.format('Left' if t == 'V' else 'Right',
F['label'],F['meta']['Description']),
'Creator': 'dadf5.py:add_stretch_tensor v{}'.format(version)
}
}
self.__add_generic_pointwise(_add_stretch_tensor,{'F':F},{'t':t})
self.__add_generic_pointwise(self._add_stretch_tensor,{'F':F},{'t':t})
def __add_generic_pointwise(self,func,dataset_mapping,args={}):
"""
General function to add pointwise data.
def job(self,group,func,datasets,args,lock):
try:
d = self._read(group,datasets,lock)
r = func(**d,**args)
return [group,r]
except Exception as err:
print('Error during calculation: {}.'.format(err))
return None
Parameters
----------
func : function
Function that calculates a new dataset from one or more datasets per HDF5 group.
dataset_mapping : dictionary
Mapping HDF5 data label to callback function argument
extra_args : dictionary, optional
Any extra arguments parsed to func.
"""
def job(args):
"""Call function with input data + extra arguments, returns results + group."""
args['results'].put({**args['func'](**args['in']),'group':args['group']})
def _read(self,group,datasets,lock):
datasets_in = {}
lock.acquire()
with h5py.File(self.fname,'r') as f:
for k,v in datasets.items():
loc = f[group+'/'+v]
datasets_in[k]={'data':loc[()],
'label':v,
'meta':{k2:v2.decode() for k2,v2 in loc.attrs.items()}}
lock.release()
return datasets_in
def __add_generic_pointwise(self,func,datasets,args={}):
env = Environment()
N_threads = int(env.options['DAMASK_NUM_THREADS'])
N_threads //=N_threads # disable for the moment
pool = multiprocessing.Pool(N_threads)
m = multiprocessing.Manager()
lock = m.Lock()
results = Queue(N_threads)
pool = util.ThreadPool(N_threads)
N_added = N_threads + 1
todo = []
# ToDo: It would be more memory efficient to read only from file when required, i.e. do to it in pool.add_task
for group in self.groups_with_datasets(dataset_mapping.values()):
with h5py.File(self.fname,'r') as f:
datasets_in = {}
for arg,label in dataset_mapping.items():
loc = f[group+'/'+label]
data = loc[()]
meta = {k:loc.attrs[k].decode() for k in loc.attrs.keys()}
datasets_in[arg] = {'data': data, 'meta': meta, 'label': label}
todo.append({'in':{**datasets_in,**args},'func':func,'group':group,'results':results})
pool.map(job, todo[:N_added]) # initialize
N_not_calculated = len(todo)
while N_not_calculated > 0:
result = results.get()
with h5py.File(self.fname,'a') as f: # write to file
dataset_out = f[result['group']].create_dataset(result['label'],data=result['data'])
for k in result['meta'].keys():
dataset_out.attrs[k] = result['meta'][k].encode()
N_not_calculated-=1
if N_added < len(todo): # add more jobs
pool.add_task(job,todo[N_added])
N_added +=1
pool.wait_completion()
groups = self.groups_with_datasets(datasets.values())
default_arg = partial(self.job,func=func,datasets=datasets,args=args,lock=lock)
for result in pool.imap_unordered(default_arg,groups):
if not result: continue
lock.acquire()
with h5py.File(self.fname, 'a') as f:
try:
dataset = f[result[0]].create_dataset(result[1]['label'],data=result[1]['data'])
for l,v in result[1]['meta'].items():
dataset.attrs[l]=v.encode()
except OSError as err:
print('Could not add dataset: {}.'.format(err))
lock.release()
pool.close()
pool.join()
def to_vtk(self,labels,mode='cell'):

View File

@ -201,57 +201,3 @@ class return_message():
def __repr__(self):
"""Return message suitable for interactive shells."""
return srepr(self.message)
class ThreadPool:
"""Pool of threads consuming tasks from a queue."""
class Worker(Thread):
"""Thread executing tasks from a given tasks queue."""
def __init__(self, tasks):
"""Worker for tasks."""
Thread.__init__(self)
self.tasks = tasks
self.daemon = True
self.start()
def run(self):
while True:
func, args, kargs = self.tasks.get()
try:
func(*args, **kargs)
except Exception as e:
# An exception happened in this thread
print(e)
finally:
# Mark this task as done, whether an exception happened or not
self.tasks.task_done()
def __init__(self, num_threads):
"""
Thread pool.
Parameters
----------
num_threads : int
number of threads
"""
self.tasks = Queue(num_threads)
for _ in range(num_threads):
self.Worker(self.tasks)
def add_task(self, func, *args, **kargs):
"""Add a task to the queue."""
self.tasks.put((func, args, kargs))
def map(self, func, args_list):
"""Add a list of tasks to the queue."""
for args in args_list:
self.add_task(func, args)
def wait_completion(self):
"""Wait for completion of all the tasks in the queue."""
self.tasks.join()