first production version for filtered plotting

This commit is contained in:
Michael Krayer 2021-06-01 12:16:10 +02:00
parent 8dd8161537
commit 3366816adf
3 changed files with 759 additions and 378 deletions

343
field.py
View File

@ -4,19 +4,74 @@ class Field3d:
assert len(origin)==3, "'origin' must be of length 3" assert len(origin)==3, "'origin' must be of length 3"
assert len(spacing)==3, "'spacing' must be of length 3" assert len(spacing)==3, "'spacing' must be of length 3"
self.data = numpy.array(data) self.data = numpy.array(data)
self.numpoints = self.data.shape
self.origin = tuple([float(x) for x in origin]) self.origin = tuple([float(x) for x in origin])
self.spacing = tuple([float(x) for x in spacing]) self.spacing = tuple([float(x) for x in spacing])
self.eps_collapse = 1e-12 self.eps_collapse = 1e-12
self._dim = None
return return
def __str__(self): def __str__(self):
str = 'Field3d with\n' str = 'Field3d with\n'
str+= ' dimension: {}, {}, {}\n'.format(*self.numpoints) str+= ' dimension: {}, {}, {}\n'.format(*self.dim())
str+= ' origin: {:G}, {:G}, {:G}\n'.format(*self.origin) str+= ' origin: {:G}, {:G}, {:G}\n'.format(*self.origin)
str+= ' spacing: {:G}, {:G}, {:G}'.format(*self.spacing) str+= ' spacing: {:G}, {:G}, {:G}\n'.format(*self.spacing)
str+= ' datatype: {}'.format(self.dtype())
return str return str
def __add__(self,other):
if isinstance(other,Field3d):
assert self.has_same_grid(other), "Grid mismatch."
return Field3d(self.data+other.data,self.origin,self.spacing)
else:
return Field3d(self.data+other,self.origin,self.spacing)
def __sub__(self,other):
if isinstance(other,Field3d):
assert self.has_same_grid(other), "Grid mismatch."
return Field3d(self.data-other.data,self.origin,self.spacing)
else:
return Field3d(self.data-other,self.origin,self.spacing)
def __mul__(self,other):
if isinstance(other,Field3d):
assert self.has_same_grid(other), "Grid mismatch."
return Field3d(self.data*other.data,self.origin,self.spacing)
else:
return Field3d(self.data*other,self.origin,self.spacing)
def __radd__(self,other):
return Field3d(other+self.data,self.origin,self.spacing)
def __rmul__(self,other):
return Field3d(other*self.data,self.origin,self.spacing)
def __pow__(self,other):
return Field3d(self.data**other,self.origin,self.spacing)
def __iadd__(self,other):
if isinstance(other,Field3d):
assert self.has_same_grid(other), "Grid mismatch."
self.data += other.data
else:
self.data += other
return self
def __isub__(self,other):
if isinstance(other,Field3d):
assert self.has_same_grid(other), "Grid mismatch."
self.data -= other.data
else:
self.data -= other
return self
def __imul__(self,other):
if isinstance(other,Field3d):
assert self.has_same_grid(other), "Grid mismatch."
self.data *= other.data
else:
self.data *= other
return self
# TBD: this should return another Field3d object # TBD: this should return another Field3d object
# def __getitem__(self,val): # def __getitem__(self,val):
# assert isinstance(val,tuple) and len(val)==3, "Field3d must be indexed by [ii,jj,kk]." # assert isinstance(val,tuple) and len(val)==3, "Field3d must be indexed by [ii,jj,kk]."
@ -45,6 +100,30 @@ class Field3d:
assert (chunk['nzl']+2*chunk['ighost'])==nz, "Invalid chunk data: nzl != chunk['data'].shape[2]" assert (chunk['nzl']+2*chunk['ighost'])==nz, "Invalid chunk data: nzl != chunk['data'].shape[2]"
return cls(chunk['data'],origin=(xo,yo,zo),spacing=(dx,dy,dz)) return cls(chunk['data'],origin=(xo,yo,zo),spacing=(dx,dy,dz))
@classmethod
def allocate(cls,dim,origin,spacing,fill=None,dtype=numpy.float64,pseudo=False):
'''Allocates an empty field, or a field filled with 'fill'.'''
assert isinstance(dim,(tuple,list,numpy.ndarray)) and len(dim)==3,\
"'dim' must be a tuple/list of length 3."
assert isinstance(origin,(tuple,list,numpy.ndarray)) and len(origin)==3,\
"'origin' must be a tuple/list of length 3."
assert isinstance(spacing,(tuple,list,numpy.ndarray)) and len(spacing)==3,\
"'spacing' must be a tuple/list of length 3."
if pseudo:
data = numpy.empty((0,0,0))
r = cls(data,origin,spacing)
r._dim = dim
return r
else:
if fill is None:
data = numpy.empty(dim,dtype=dtype)
else:
data = numpy.full(dim,fill,dtype=dtype)
return cls(data,origin,spacing)
def copy(self):
return Field3d(self.data.copy(),self.origin,self.spacing)
def insert_subfield(self,subfield): def insert_subfield(self,subfield):
assert all([abs(subfield.spacing[ii]-self.spacing[ii])<self.eps_collapse assert all([abs(subfield.spacing[ii]-self.spacing[ii])<self.eps_collapse
for ii in range(3)]), "spacing differs." for ii in range(3)]), "spacing differs."
@ -54,16 +133,16 @@ class Field3d:
assert all(self.is_within_bounds(subfield.endpoint(),axis=None)), "subfield endpoint is out-of-bounds." assert all(self.is_within_bounds(subfield.endpoint(),axis=None)), "subfield endpoint is out-of-bounds."
#ib,jb,kb = [int(round((subfield.origin[ii]-self.origin[ii])/self.spacing[ii])) for ii in range(3)] #ib,jb,kb = [int(round((subfield.origin[ii]-self.origin[ii])/self.spacing[ii])) for ii in range(3)]
ib,jb,kb = self.nearest_gridpoint(subfield.origin,axis=None) ib,jb,kb = self.nearest_gridpoint(subfield.origin,axis=None)
nx,ny,nz = subfield.numpoints nx,ny,nz = subfield.dim()
self.data[ib:ib+nx,jb:jb+ny,kb:kb+nz] = subfield.data[:,:,:] self.data[ib:ib+nx,jb:jb+ny,kb:kb+nz] = subfield.data[:,:,:]
return return
def extract_subfield(self,idx_origin,numpoints,stride=(1,1,1)): def extract_subfield(self,idx_origin,dim,stride=(1,1,1)):
assert all(idx_origin[ii]>=0 and idx_origin[ii]<self.numpoints[ii] for ii in range(3)),\ assert all(idx_origin[ii]>=0 and idx_origin[ii]<self.dim(axis=ii) for ii in range(3)),\
"'origin' is out-of-bounds." "'origin' is out-of-bounds."
assert all(idx_origin[ii]+stride[ii]*(numpoints[ii]-1)<self.numpoints[ii] for ii in range(3)),\ assert all(idx_origin[ii]+stride[ii]*(dim[ii]-1)<self.dim(axis=ii) for ii in range(3)),\
"endpoint is out-of-bounds." "endpoint is out-of-bounds."
sl = tuple(slice(idx_origin[ii],idx_origin[ii]+stride[ii]*numpoints[ii],stride[ii]) for ii in range(3)) sl = tuple(slice(idx_origin[ii],idx_origin[ii]+stride[ii]*dim[ii],stride[ii]) for ii in range(3))
origin = self.coordinate(idx_origin) origin = self.coordinate(idx_origin)
spacing = tuple(self.spacing[ii]*stride[ii] for ii in range(3)) spacing = tuple(self.spacing[ii]*stride[ii] for ii in range(3))
data = self.data[sl].copy() data = self.data[sl].copy()
@ -74,7 +153,7 @@ class Field3d:
assert len(idx)==3, "If 'axis' is None, 'idx' must be a tuple/list of length 3." assert len(idx)==3, "If 'axis' is None, 'idx' must be a tuple/list of length 3."
return tuple(self.coordinate(idx[ii],axis=ii) for ii in range(3)) return tuple(self.coordinate(idx[ii],axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
assert idx<self.numpoints[axis], "'idx' is out-of-bounds." assert idx<self.dim(axis=axis), "'idx' is out-of-bounds."
return self.origin[axis]+idx*self.spacing[axis] return self.origin[axis]+idx*self.spacing[axis]
def nearest_gridpoint(self,coord,axis=None,lower=False): def nearest_gridpoint(self,coord,axis=None,lower=False):
@ -103,61 +182,84 @@ class Field3d:
return tuple(self.is_within_bounds(coord[ii],axis=ii) for ii in range(3)) return tuple(self.is_within_bounds(coord[ii],axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
idx_nearest = self.nearest_gridpoint(coord,axis=axis) idx_nearest = self.nearest_gridpoint(coord,axis=axis)
if idx_nearest>0 and idx_nearest<self.numpoints[axis]-1: if idx_nearest>0 and idx_nearest<self.dim(axis=axis)-1:
return True return True
dist_nearest = self.distance_to_nearest_gridpoint(coord,axis=axis) dist_nearest = self.distance_to_nearest_gridpoint(coord,axis=axis)
if (idx_nearest==0 or idx_nearest==self.numpoints[axis]-1) and abs(dist_nearest)<self.eps_collapse: if (idx_nearest==0 or idx_nearest==self.dim(axis=axis)-1) and abs(dist_nearest)<self.eps_collapse:
return True return True
else: else:
return False return False
def has_same_grid(self,other):
if not self.has_same_origin(other): return False
if not self.has_same_spacing(other): return False
if not self.has_same_origin(other): return False
return True
def has_same_origin(self,other,axis=None):
if axis is None:
return all([self.has_same_origin(other,axis=ii) for ii in range(3)])
origin = other.origin if isinstance(other,Field3d) else other
return abs(origin[axis]-self.origin[axis])<self.eps_collapse
def has_same_spacing(self,other,axis=None):
if axis is None:
return all([self.has_same_spacing(other,axis=ii) for ii in range(3)])
spacing = other.spacing if isinstance(other,Field3d) else other
return abs(spacing[axis]-self.spacing[axis])<self.eps_collapse
def has_same_dim(self,other,axis=None):
if axis is None:
return all([self.has_same_dim(other,axis=ii) for ii in range(3)])
dim = other.dim(axis=axis) if isinstance(other,Field3d) else other
return dim==self.dim(axis=axis)
def dim(self,axis=None): def dim(self,axis=None):
if axis is None: if axis is None:
return tuple(self.dim(axis=ii) for ii in range(3)) return tuple(self.dim(axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
return self.numpoints[axis] if self._dim is not None:
return self._dim[axis]
else:
return self.data.shape[axis]
def endpoint(self,axis=None): def endpoint(self,axis=None):
if axis is None: if axis is None:
return tuple(self.endpoint(axis=ii) for ii in range(3)) return tuple(self.endpoint(axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
return self.origin[axis]+(self.numpoints[axis]-1)*self.spacing[axis] return self.origin[axis]+(self.dim(axis=axis)-1)*self.spacing[axis]
def derivative(self,axis,only_keep_interior=False,add_border='before',preserve_origin=False): def dtype(self):
if axis is None: return self.data.dtype
return tuple(self.derivative(axis,preserve_grid=preserve_grid,padding=padding,numpad=numpad)
for axis in range(0,3)) def convert_dtype(self,dtype):
self.data = self.data.astype(dtype,copy=False)
return
def derivative(self,axis,only_keep_interior=False,shift_origin='before'):
'''Computes derivative wrt to direction 'axis' with 2nd order finite differences
centered between the origin grid points'''
from scipy import ndimage
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
if not preserve_origin: origin = list(self.origin)
origin = list(self.origin) assert shift_origin in ('before','after'), "'shift_origin' must be one of {'before','after'}."
numpoints = list(self.numpoints) if shift_origin=='left':
origin[axis] += 0.5*self.spacing[axis] corr_orig = 0
numpoints[axis] -= 1 origin[axis] -= 0.5*self.spacing[axis]
if not only_keep_interior:
assert add_border in ('before','after'), "'add_border' must be one of {'before','after'}."
origin,numpoints,sl_out = padding(origin,self.spacing,numpoints,add_border,1)
else:
sl_out = tuple(slice(None),slice(None),slice(None))
data = numpy.zeros(numpoints,dtype=self.data.dtype)
# 2nd order FD with h = dx/2
slhi = 3*[slice(None)]
sllo = 3*[slice(None)]
slhi[axis] = slice(1,None)
sllo[axis] = slice(0,-1)
slhi = tuple(slhi)
sllo = tuple(sllo)
data[sl_out] = (1./self.spacing[axis])*(self.data[slhi]-self.data[sllo])
else: else:
origin,numpoints,sl_out = padding(self.origin,self.spacing,self.numpoints,padding,numpad) corr_orig = -1
data = numpy.zeros(numpoints,dtype=self.data.dtype) origin[axis] += 0.5*self.spacing[axis]
# 2nd order FD with h = dx, one-sided at boundaries data = ndimage.correlate1d(self.data,numpy.array([-1.,1.])/self.spacing[axis],axis=axis,
# https://numpy.org/doc/stable/reference/generated/numpy.gradient.html mode='constant',cval=numpy.nan,origin=corr_orig)
data[sl_out] = numpy.gradient(self.data,self.spacing[axis],axis=axis,edge_order=2) if only_keep_interior:
if only_keep_interior: sl = 3*[slice(None)]
sl_out = 3*[slice(None)] if shift_origin=='before':
sl_out[axis] = slice(1,-1) sl[axis] = slice(-1,None)
data = data[tuple(sl_out)]
origin[axis] += self.spacing[axis] origin[axis] += self.spacing[axis]
else:
sl[axis] = slice(0,-1)
data = data[tuple(sl)]
return Field3d(data,origin,self.spacing) return Field3d(data,origin,self.spacing)
def gradient(self,axis,preserve_origin=False,only_keep_interior=False,add_border='before'): def gradient(self,axis,preserve_origin=False,only_keep_interior=False,add_border='before'):
@ -167,9 +269,9 @@ class Field3d:
def laplacian(self,only_keep_interior=False): def laplacian(self,only_keep_interior=False):
'''Computes the Laplacian of a field.''' '''Computes the Laplacian of a field.'''
from scipy import ndimage from scipy import ndimage
data = ndimage.correlate1d(self.data,[1.,-2.,1.]/self.spacing[0],axis=0,mode='constant',cval=numpy.nan,origin=0) data = ndimage.correlate1d(self.data,numpy.array([1.,-2.,1.])/self.spacing[0]**2,axis=0,mode='constant',cval=numpy.nan,origin=0)
data += ndimage.correlate1d(self.data,[1.,-2.,1.]/self.spacing[1],axis=1,mode='constant',cval=numpy.nan,origin=0) data += ndimage.correlate1d(self.data,numpy.array([1.,-2.,1.])/self.spacing[1]**2,axis=1,mode='constant',cval=numpy.nan,origin=0)
data += ndimage.correlate1d(self.data,[1.,-2.,1.]/self.spacing[2],axis=2,mode='constant',cval=numpy.nan,origin=0) data += ndimage.correlate1d(self.data,numpy.array([1.,-2.,1.])/self.spacing[2]**2,axis=2,mode='constant',cval=numpy.nan,origin=0)
origin = list(self.origin) origin = list(self.origin)
if only_keep_interior: if only_keep_interior:
data = data[1:-1,1:-1,1:-1] data = data[1:-1,1:-1,1:-1]
@ -191,7 +293,7 @@ class Field3d:
if integrate_axis[axis]: if integrate_axis[axis]:
axes.append(axis) axes.append(axis)
if average: if average:
weight *= self.numpoints[axis] weight *= self.dim(axis=axis)
else: else:
weight *= self.spacing[axis] weight *= self.spacing[axis]
axes = tuple(axes) axes = tuple(axes)
@ -240,49 +342,89 @@ class Field3d:
radius.append(int(truncate*sigma_img[ii]+0.5)) radius.append(int(truncate*sigma_img[ii]+0.5))
return tuple(radius) return tuple(radius)
def shift_origin(self,rel_shift): # def shift_origin(self,rel_shift):
raise NotImplementedError("Routine has not been verified yet.") # raise NotImplementedError("Routine has not been verified yet.")
#TBD: verify this routine # #TBD: verify this routine
# assert isinstance(rel_shift,(tuple,list,numpy.ndarray)) and len(rel_shift)==3,\
# "'shift' must be tuple/list with length 3."
# assert all([rel_shift[ii]>-1.0 and rel_shift[ii]<1.0 for ii in range(3)]),\
# "'shift' must be in (-1.0,1.0)."
# #data = numpy.full(self.dim,numpy.nan)
# data = self.data.copy()
# origin = list(self.origin)
# for axis in range(3):
# if abs(rel_shift[axis])<self.eps_collapse:
# continue
# origin[axis] += rel_shift[axis]*self.spacing[axis]
# sl_hi = 3*[slice(None)]
# sl_lo = 3*[slice(None)]
# sl_out = 3*[slice(None)]
# sl_clr = 3*[slice(None)]
# sl_hi[axis] = slice(1,None)
# sl_lo[axis] = slice(0,-1)
# if rel_shift[ii]<0.0:
# w = 1.0+rel_shift[axis]
# sl_out[axis] = slice(1,None)
# sl_clr[axis] = slice(0,1)
# else:
# w = rel_shift[axis]
# sl_out[axis] = slice(0,-1)
# sl_clr[axis] = slice(-1,None)
# sl_hi = tuple(sl_hi)
# sl_lo = tuple(sl_lo)
# sl_out = tuple(sl_out)
# sl_clr = tuple(sl_clr)
# data[sl_out] = (1.0-w)*data[sl_lo] + w*data[sl_hi]
# data[sl_clr] = numpy.nan
# return Field3d(data,origin,self.spacing)
def shift_origin(self,rel_shift,only_keep_interior=False):
'''Shifts the origin of a field by multiple of spacing.'''
from scipy import ndimage
assert isinstance(rel_shift,(tuple,list,numpy.ndarray)) and len(rel_shift)==3,\ assert isinstance(rel_shift,(tuple,list,numpy.ndarray)) and len(rel_shift)==3,\
"'shift' must be tuple/list with length 3." "'shift' must be tuple/list with length 3."
assert all([rel_shift[ii]>-1.0 and rel_shift[ii]<1.0 for ii in range(3)]),\ assert all([rel_shift[ii]>=-1.0 and rel_shift[ii]<=1.0 for ii in range(3)]),\
"'shift' must be in (-1.0,1.0)." "'shift' must be in (-1.0,1.0)."
#data = numpy.full(self.numpoints,numpy.nan) data = self.data.copy()
data = self.data.copy()
origin = list(self.origin) origin = list(self.origin)
sl = 3*[slice(None)]
for axis in range(3): for axis in range(3):
if abs(rel_shift[axis])<self.eps_collapse: if abs(rel_shift[axis])<self.eps_collapse:
continue continue
origin[axis] += rel_shift[axis]*self.spacing[axis] elif rel_shift[axis]>0:
sl_hi = 3*[slice(None)] weights = (1.0-rel_shift[axis],rel_shift[axis])
sl_lo = 3*[slice(None)] data = ndimage.correlate1d(data,weights,axis=axis,mode='constant',cval=numpy.nan,origin=-1)
sl_out = 3*[slice(None)] origin[axis] += rel_shift[axis]*self.spacing[axis]
sl_clr = 3*[slice(None)] if only_keep_interior:
sl_hi[axis] = slice(1,None) sl[axis] = slice(0,-1)
sl_lo[axis] = slice(0,-1)
if rel_shift[ii]<0.0:
w = 1.0+rel_shift[axis]
sl_out[axis] = slice(1,None)
sl_clr[axis] = slice(0,1)
else: else:
w = rel_shift[axis] weights = (-rel_shift[axis],1.0+rel_shift[axis])
sl_out[axis] = slice(0,-1) data = ndimage.correlate1d(data,weights,axis=axis,mode='constant',cval=numpy.nan,origin=0)
sl_clr[axis] = slice(-1,None) origin[axis] += rel_shift[axis]*self.spacing[axis]
sl_hi = tuple(sl_hi) if only_keep_interior:
sl_lo = tuple(sl_lo) sl[axis] = slice(1,None)
sl_out = tuple(sl_out) origin[axis] += self.spacing[axis]
sl_clr = tuple(sl_clr) if only_keep_interior:
data[sl_out] = (1.0-w)*data[sl_lo] + w*data[sl_hi] data = data[sl]
data[sl_clr] = numpy.nan
return Field3d(data,origin,self.spacing) return Field3d(data,origin,self.spacing)
def change_grid(self,origin,spacing,numpoints,padding=None,numpad=1): def relative_shift(self,field):
'''Compute the relative shift (in terms of spacing) to shift self onto field.'''
assert self.has_same_spacing(field), "spacing differs."
rel_shift = [0.0,0.0,0.0]
for axis in range(3):
dist = field.origin[axis]-self.origin[axis]
if abs(dist)>self.eps_collapse:
rel_shift[axis] = dist/self.spacing[axis]
return tuple(rel_shift)
def change_grid(self,origin,spacing,dim,padding=None,numpad=1):
assert all([origin[ii]>=self.origin[ii] for ii in range(0,3)]), "New origin is out of bounds." assert all([origin[ii]>=self.origin[ii] for ii in range(0,3)]), "New origin is out of bounds."
endpoint = [origin[ii]+(numpoints[ii]-1)*spacing[ii] for ii in range(0,3)] endpoint = [origin[ii]+(dim[ii]-1)*spacing[ii] for ii in range(0,3)]
assert all([endpoint[ii]<=self.endpoint(ii) for ii in range(0,3)]), "New end point is out of bounds." assert all([endpoint[ii]<=self.endpoint(ii) for ii in range(0,3)]), "New end point is out of bounds."
# Allocate (possibly padded array) # Allocate (possibly padded array)
origin_pad,numpoints_pad,sl_out = padding(origin,spacing,numpoints,padding,numpad): origin_pad,dim_pad,sl_out = padding(origin,spacing,dim,padding,numpad)
data = numpy.zeros(numpoints_pad,dtype=self.data.dtype) data = numpy.zeros(dim_pad,dtype=self.data.dtype)
# Trilinear interpolation # Trilinear interpolation
if numpy.allclose(spacing,self.spacing): if numpy.allclose(spacing,self.spacing):
# spacing is the same: we can construct universal weights for the stencil # spacing is the same: we can construct universal weights for the stencil
@ -295,14 +437,14 @@ class Field3d:
for kk in range(0,2): for kk in range(0,2):
if c[ii,jj,kk]>self.eps_collapse: if c[ii,jj,kk]>self.eps_collapse:
data[sl_out] += c[ii,jj,kk]*self.data[ data[sl_out] += c[ii,jj,kk]*self.data[
i0+ii:i0+ii+numpoints[0], i0+ii:i0+ii+dim[0],
j0+jj:j0+jj+numpoints[1], j0+jj:j0+jj+dim[1],
k0+kk:k0+kk+numpoints[2]] k0+kk:k0+kk+dim[2]]
else: else:
data_ref = data[sl_out] data_ref = data[sl_out]
for ii in range(0,numpoints[0]): for ii in range(0,dim[0]):
for jj in range(0,numpoints[1]): for jj in range(0,dim[1]):
for kk in range(0,numpoints[2]): for kk in range(0,dim[2]):
coord = ( coord = (
origin[0]+ii*spacing[0], origin[0]+ii*spacing[0],
origin[1]+jj*spacing[1], origin[1]+jj*spacing[1],
@ -326,36 +468,36 @@ class Field3d:
return val return val
@staticmethod @staticmethod
def padding(origin,spacing,numpoints,padding,numpad): def padding(origin,spacing,dim,padding,numpad):
if isinstance(numpad,int): if isinstance(numpad,int):
numpad = numpy.fill(3,numpad,dtype=int) numpad = numpy.fill(3,numpad,dtype=int)
else: else:
numpad = numpy.array(numpad,dtype=int) numpad = numpy.array(numpad,dtype=int)
assert len(numpad)==3, "'numpad' must be either an integer or tuple/list of length 3." assert len(numpad)==3, "'numpad' must be either an integer or tuple/list of length 3."
origin_pad = numpy.array(origin) origin_pad = numpy.array(origin)
numpoints_pad = numpy.array(numpoints) dim_pad = numpy.array(dim)
sl_out = [slice(None),slice(None),slice(None)] sl_out = [slice(None),slice(None),slice(None)]
if padding is not None: if padding is not None:
if padding=='before': if padding=='before':
numpoints_pad += numpad dim_pad += numpad
origin_pad -= numpad*spacing origin_pad -= numpad*spacing
for axis in range(3): for axis in range(3):
sl_out[axis] = slice(numpad[axis],None) sl_out[axis] = slice(numpad[axis],None)
elif padding=='after': elif padding=='after':
numpoints_pad += numpad dim_pad += numpad
for axis in range(3): for axis in range(3):
sl_out[axis] = slice(0,-numpad[axis]) sl_out[axis] = slice(0,-numpad[axis])
elif padding=='both': elif padding=='both':
numpoints_pad += 2*numpad dim_pad += 2*numpad
origin_pad -= numpad*spacing origin_pad -= numpad*spacing
for axis in range(3): for axis in range(3):
sl_out[axis] = slice(numpad[axis],-numpad[axis]) sl_out[axis] = slice(numpad[axis],-numpad[axis])
else: else:
raise ValueError("'padding' must either be None or one of {'before','after','both'}.") raise ValueError("'padding' must either be None or one of {'before','after','both'}.")
sl_out = tuple(sl_out) sl_out = tuple(sl_out)
origin_pad = tuple(origin_pad) origin_pad = tuple(origin_pad)
numpoints_pad = tuple(numpoints_pad) dim_pad = tuple(dim_pad)
return (origin_pad,numpoints_pad,sl_out) return (origin_pad,dim_pad,sl_out)
def weights_trilinear(self,rel_dist): def weights_trilinear(self,rel_dist):
assert len(rel_dist)==3, "len(rel_dist) must be 3." assert len(rel_dist)==3, "len(rel_dist) must be 3."
@ -408,6 +550,19 @@ class Field3d:
"'origin' must be tuple of length 3." "'origin' must be tuple of length 3."
return self.to_vtk(deep=deep).slice(normal=normal,origin=origin) return self.to_vtk(deep=deep).slice(normal=normal,origin=origin)
def gaussian_filter_umean_channel(array,spacing,sigma,truncate=4.0):
'''Applies a Gaussian filter to a numpy array of dimension (1,ny,1) which
contains the mean streamwise velocity of the channel (or possibly sth else).
Since yu[0] = c and yu[-1] = d, we can use scipy's mirror settings and don't
need ghost cells.'''
from scipy import ndimage
assert array.ndim==3, "Expected an array with three dimensions/axes."
assert array.shape[0]==1 and array.shape[1]>1 and array.shape[2]==1,\
"Expected an array with shape (1,ny,1)."
sigma_img = sigma/spacing
array = ndimage.gaussian_filter1d(array,sigma_img,axis=1,truncate=truncate,mode='mirror')
return array
class ChunkIterator: class ChunkIterator:
'''Iterates through all chunks. 'snapshot' must be an instance '''Iterates through all chunks. 'snapshot' must be an instance
of a class which returns a Field3d from the method call of a class which returns a Field3d from the method call

View File

@ -1,22 +1,43 @@
import numpy as np
class PPP: class PPP:
"""Parallel Python Postprocessor for suspend""" """Parallel Python Postprocessor for suspend"""
def __init__(self,comm,func_load,num_ghost,precision,origin,spacing,periodicity,bounds,proc,chunks_per_proc): def __init__(self,comm,func_load,
'''Constructor: except for comm, only rank 0 needs initialized data.''' num_ghost,chunks_per_proc,origin,spacing,periodicity,bounds,
self.comm = comm proc_grid,nxp,nyp,nzp,
self.rank = comm.Get_rank() proc_grid_ext,nxp_ext,nyp_ext,nzp_ext,
self.func_load = func_load nghbr,field,symmetries):
self.init_settings(num_ghost,precision) '''Class constructor: it is expected that all communcation is done in the corresponding
self.init_domain(origin,spacing,periodicity,bounds) class methods, such that every rank already holds the required information.'''
self.init_procgrid(proc,chunks_per_proc) # Initialize object with passed arguments
self.field = {} self.comm = comm
self.symmetries = {} self.func_load = func_load
return self.num_ghost = num_ghost
self.chunks_per_proc = chunks_per_proc
self.origin = origin
self.spacing = spacing
self.periodicity = periodicity
self.bounds = bounds
self.proc_grid = proc_grid
self.nxp,self.nyp,self.nzp = nxp,nyp,nzp
self.proc_grid_ext = proc_grid_ext
self.nxp_ext,self.nyp_ext,self.nzp_ext = nxp_ext,nyp_ext,nzp_ext
self.nghbr = nghbr
self.field = field
self.symmetries = symmetries
# Initialize some deduced variables
self.rank = comm.Get_rank()
self.nproc = nxp*nyp*nzp
self.ip,self.jp,self.kp = self.position_from_rank(self.rank,external=False)
self.nghx,self.nghy,self.nghz = num_ghost
self.xperiodic,self.yperiodic,self.zperiodic = periodicity
return
@classmethod @classmethod
def from_snapshot(cls,snap,chunks_per_proc=(1,1,1),num_ghost=(1,1,1),precision='float64'): def from_snapshot(cls,snap,chunks_per_proc=(1,1,1),num_ghost=(1,1,1)):
from mpi4py import MPI from mpi4py import MPI
comm = MPI.COMM_WORLD comm = MPI.COMM_WORLD
rank = comm.Get_rank() rank = comm.Get_rank()
# Let rank 0 read the processor grid as well as some info and then broadcast it
if rank==0: if rank==0:
proc = snap.procgrid() proc = snap.procgrid()
origin = snap.origin() origin = snap.origin()
@ -24,182 +45,53 @@ class PPP:
periodicity = snap.periodicity() periodicity = snap.periodicity()
bounds = snap.bounds() bounds = snap.bounds()
else: else:
proc = None proc,origin,spacing,periodicity,bounds = 5*[None]
origin = None proc = comm.bcast(proc ,root=0)
spacing = None origin = comm.bcast(origin ,root=0)
periodicity = None spacing = comm.bcast(spacing ,root=0)
bounds = None periodicity = comm.bcast(periodicity,root=0)
bounds = comm.bcast(bounds ,root=0)
# Make sure all processors got the same arguments
chunks_per_proc = comm.bcast(chunks_per_proc,root=0)
num_ghost = comm.bcast(num_ghost,root=0)
# Set data loading function
func_load = snap.field_chunk func_load = snap.field_chunk
return cls(comm,func_load,num_ghost,precision,origin,spacing,periodicity,bounds,proc,chunks_per_proc) # Construct new processor grid
(proc_grid,nxp,nyp,nzp,
proc_grid_ext,nxp_ext,nyp_ext,nzp_ext,
nghbr) = PPP._construct_procgrid(comm,proc,chunks_per_proc,num_ghost,periodicity)
# Initially, no field is loaded
field = {}
symmetries = {}
return cls(comm,func_load,
num_ghost,chunks_per_proc,origin,spacing,periodicity,bounds,
proc_grid,nxp,nyp,nzp,
proc_grid_ext,nxp_ext,nyp_ext,nzp_ext,
nghbr,field,symmetries)
def init_settings(self,num_ghost,precision): @classmethod
'''Initializes PPP settings for all processors.''' def from_state(cls,filename):
# TBD: some assertions import pickle
self.num_ghost = self.comm.bcast(num_ghost,root=0) from mpi4py import MPI
self.precision = self.comm.bcast(precision,root=0) comm = MPI.COMM_WORLD
# Some shortcuts rank = comm.Get_rank()
self.nghx,self.nghy,self.nghz = self.num_ghost fin = filename+'.{:05d}.pickle'.format(rank)
with open(fin,"rb") as f:
payload = pickle.load(f)
(num_ghost,chunks_per_proc,origin,spacing,periodicity,bounds,
proc_grid,nxp,nyp,nzp,nghbr,field,symmetries) = payload
assert nxp*nyp*nzp==comm.Get_size(), "The loaded state requires {} processors, but "\
"we are currently running with {}.".format(nxp*nyp*nzp,comm.Get_size())
func_load = None
proc_grid_ext = None
nxp_ext,nyp_ext,nzp_ext = 3*[None]
return cls(comm,func_load,
num_ghost,chunks_per_proc,origin,spacing,periodicity,bounds,
proc_grid,nxp,nyp,nzp,
proc_grid_ext,nxp_ext,nyp_ext,nzp_ext,
nghbr,field,symmetries)
def init_symmetries(self,key,mirror=(True,True)): def load_field(self,key,io_limit=None,barrier=False,dtype=np.float64):
'''Sets the symmetries for ghost cells behind the wall'''
# Example: wall in y
# No-slip boundary (no mirror) free-slip boundary (mirror)
# u -> -u u -> u
# v -> -v v -> -v
# w -> -w w -> w
# p -> p p -> p
import numpy as np
self.symmetries[key] = np.zeros((3,3,3),dtype='i')
if not self.xperiodic:
if key=='u':
self.symmetries[key][0,1,1] = -1
self.symmetries[key][2,1,1] = -1
else:
self.symmetries[key][0,1,1] = 1 if mirror[0] else -1
self.symmetries[key][2,1,1] = 1 if mirror[1] else -1
if not self.yperiodic:
if key=='v':
self.symmetries[key][1,0,1] = -1
self.symmetries[key][1,2,1] = -1
else:
self.symmetries[key][1,0,1] = 1 if mirror[0] else -1
self.symmetries[key][1,2,1] = 1 if mirror[1] else -1
if not self.zperiodic:
if key=='w':
self.symmetries[key][1,1,0] = -1
self.symmetries[key][1,1,2] = -1
else:
self.symmetries[key][1,1,0] = 1 if mirror[0] else -1
self.symmetries[key][1,1,2] = 1 if mirror[1] else -1
def init_domain(self,origin,spacing,periodicity,bounds):
'''Sets up domain information for all processors'''
# TBD: some assertions
self.origin = self.comm.bcast(origin,root=0)
self.spacing = self.comm.bcast(spacing,root=0)
self.periodicity = self.comm.bcast(periodicity,root=0)
self.bounds = self.comm.bcast(bounds,root=0)
# Some shortcuts
self.xperiodic,self.yperiodic,self.zperiodic = self.periodicity
return
def init_procgrid(self,proc,chunks_per_proc):
# Note: requires nghx, xperiodic to be set
'''Read input processor grid, compute processor grid for workers'''
import numpy as np
self.chunks_per_proc = self.comm.bcast(chunks_per_proc,root=0)
self.nxcpp = chunks_per_proc[0]
self.nycpp = chunks_per_proc[1]
self.nzcpp = chunks_per_proc[2]
if self.rank==0:
# Assert proc and add it to class
assert all(k in proc for k in ('u','v','w','p','s')), "'proc' must be a dictionary with "\
"keys 'u','v','w','p','s'"
for key in proc:
assert len(proc[key])==6, "Entries of 'proc' must have length of 6."
proc_grid_ext = proc
# Initialize chunks per processor
# Verify that this processor grid can be redistributed onto the requested processor layout
nxp_ext = len(proc_grid_ext['u'][0])
nyp_ext = len(proc_grid_ext['u'][2])
nzp_ext = len(proc_grid_ext['u'][4])
nproc_ext = nxp_ext*nyp_ext*nzp_ext
#
assert nxp_ext%self.nxcpp==0, "Number of processors must be divisible by the number "\
"of processors per process. (nxp_ext={}, nxcpp={})".format(
nxp_ext,self.nxcpp)
assert nyp_ext%self.nycpp==0, "Number of processors must be divisible by the number "\
"of processors per process. (nyp_ext={}, nycpp={})".format(
nyp_ext,self.nycpp)
assert nzp_ext%self.nzcpp==0, "Number of processors must be divisible by the number "\
"of processors per process. (nzp_ext={}, nzcpp={})".format(
nzp_ext,self.nzcpp)
# Determine the new processor layout and verify total number of MPI processes
nxp = nxp_ext//self.nxcpp
nyp = nyp_ext//self.nycpp
nzp = nzp_ext//self.nzcpp
nproc = nxp*nyp*nzp
assert nproc==self.comm.Get_size(), "Number of MPI processes does not match the requested "\
"processor layout. (MPI procs: {}, required procs: {})".format(
self.comm.Get_size(),nproc)
# Construct internal processor grid
proc_grid = {}
for key in proc_grid_ext:
proc_grid[key] = [None]*6
proc_grid[key][0] = proc_grid_ext[key][0][0::self.nxcpp]
proc_grid[key][1] = proc_grid_ext[key][1][self.nxcpp-1::self.nxcpp]
proc_grid[key][2] = proc_grid_ext[key][2][0::self.nycpp]
proc_grid[key][3] = proc_grid_ext[key][3][self.nycpp-1::self.nycpp]
proc_grid[key][4] = proc_grid_ext[key][4][0::self.nzcpp]
proc_grid[key][5] = proc_grid_ext[key][5][self.nzcpp-1::self.nzcpp]
else:
proc_grid_ext = None
proc_grid = None
nxp_ext,nyp_ext,nzp_ext,nproc_ext = None,None,None,None
nxp,nyp,nzp,nproc = None,None,None,None
# Broadcast the data
self.proc_grid_ext = self.comm.bcast(proc_grid_ext,root=0)
self.proc_grid = self.comm.bcast(proc_grid,root=0)
self.nxp_ext,self.nyp_ext,\
self.nzp_ext,self.nproc_ext = self.comm.bcast((nxp_ext,nyp_ext,nzp_ext,nproc_ext),root=0)
self.nxp,self.nyp,\
self.nzp,self.nproc = self.comm.bcast((nxp,nyp,nzp,nproc),root=0)
# Get position in processor grid
self.ip,self.jp,self.kp = self.position_from_rank(self.rank,external=False)
# Determine local grid indices and size
for key in self.proc_grid:
nxl = self.proc_grid[key][1][self.ip]-self.proc_grid[key][0][self.ip]+1
nyl = self.proc_grid[key][3][self.jp]-self.proc_grid[key][2][self.jp]+1
nzl = self.proc_grid[key][5][self.kp]-self.proc_grid[key][4][self.kp]+1
# Verify that local grid size is not smaller than ghost cell size
assert (nxl>=self.nghx and nyl>=self.nghy and nzl>=self.nghz),\
"Local grid size must be greater than number "\
"of ghost cells in each direction!"
# Initialize neighbor array
nghbr = np.empty((3,3,3),dtype='int')
# wrap-around x
ipl = self.ip-1
if ipl<0:
if self.xperiodic: ipl = self.nxp-1
else: ipl = -1
ipr = self.ip+1
if ipr>self.nxp-1:
if self.xperiodic: ipr = 0
else: ipr = -1
inghbr = (ipl,self.ip,ipr)
# wrap-around y
jpl = self.jp-1
if jpl<0:
if self.yperiodic: jpl = self.nyp-1
else: jpl = -1
jpr = self.jp+1
if jpr>self.nyp-1:
if self.yperiodic: jpr = 0
else: jpr = -1
jnghbr = (jpl,self.jp,jpr)
# wrap-around z
kpl = self.kp-1
if kpl<0:
if self.zperiodic: kpl = self.nzp-1
else: kpl = -1
kpr = self.kp+1
if kpr>self.nzp-1:
if self.zperiodic: kpr = 0
else: kpr = -1
knghbr = (kpl,self.kp,kpr)
# Construct array of neighbors
for ip in range(3):
for jp in range(3):
for kp in range(3):
# Assign rank to neighbor array
if inghbr[ip]<0 or jnghbr[jp]<0 or knghbr[kp]<0:
nghbr[ip,jp,kp] = -1
else:
nghbr[ip,jp,kp] = self.rank_from_position(inghbr[ip],jnghbr[jp],knghbr[kp],external=False)
# Save neighbors as class variable
self.nghbr = nghbr
def load_field(self,key,io_limit=None,barrier=False):
'''Loads the required chunks from file''' '''Loads the required chunks from file'''
from .field import Field3d from .field import Field3d
import numpy as np import numpy as np
@ -226,7 +118,7 @@ class PPP:
data = np.empty( data = np.empty(
(nxl+2*self.nghx, (nxl+2*self.nghx,
nyl+2*self.nghy, nyl+2*self.nghy,
nzl+2*self.nghz),dtype=self.precision) nzl+2*self.nghz),dtype=dtype)
# Compute origin of subfield # Compute origin of subfield
origin = (self.origin[key][0]+(ib-1-self.nghx)*self.spacing[0], origin = (self.origin[key][0]+(ib-1-self.nghx)*self.spacing[0],
self.origin[key][1]+(jb-1-self.nghy)*self.spacing[1], self.origin[key][1]+(jb-1-self.nghy)*self.spacing[1],
@ -257,34 +149,32 @@ class PPP:
# Exchange ghost cells # Exchange ghost cells
self.exchange_ghost_cells(key) self.exchange_ghost_cells(key)
# Initialize symmetries and impose BC # Initialize symmetries and impose BC
self.init_symmetries(key) self.init_symmetry(key)
self.impose_boundary_conditions(key) self.impose_boundary_conditions(key)
# Syncronize processes if requested # Syncronize processes if requested
if barrier: self.comm.Barrier() if barrier: self.comm.Barrier()
def differentiate(self,key,axis,key_out=None): def differentiate(self,key,axis,key_out=None,on_pressure_grid=False):
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
if key_out is None: if key_out is None:
key_out = key+('x','y','z')[axis] key_out = 'd'+key+('/dx','/dy','/dz')[axis]
origin = list(self.origin) origin = list(self.origin[key])
shifting_state = self.shifting_state(key,axis=axis) shifting_state = self.shifting_state(key,axis=axis)
if shifting_state==-1: if shifting_state==-1:
padding = 'after' shift_origin = 'after'
origin[axis] += 0.5*self.spacing[axis] origin[axis] += 0.5*self.spacing[axis]
elif shifting_state==0: elif shifting_state==0:
padding = 'before' shift_origin = 'before'
origin[axis] -= 0.5*self.spacing[axis] origin[axis] -= 0.5*self.spacing[axis]
elif shifting_state==1: elif shifting_state==1:
padding = 'before' shift_origin = 'before'
origin[axis] -= 0.5*self.spacing[axis] origin[axis] -= 0.5*self.spacing[axis]
else: else:
raise ValueError("Invalid shifting state.") raise ValueError("Invalid shifting state.")
self.field[key_out] = self.field[key].derivative(axis,padding=padding) self.field[key_out] = self.field[key].derivative(axis,only_keep_interior=False,shift_origin=shift_origin)
self.origin[key_out] = tuple(origin) self.origin[key_out] = tuple(origin)
self.spacing[key_out] = self.spacing[key].copy() self.symmetries[key_out] = self.symmetries[key].copy()
self.symmetries[key_out] = self.symmetries[key].copy() self.proc_grid[key_out] = self.proc_grid[key].copy()
self.proc_grid[key_out] = self.proc_grid[key].copy()
self.proc_grid_ext[key_out] = self.proc_grid_ext[key].copy()
if axis==0: if axis==0:
self.symmetries[key_out][0,1,1] = -self.symmetries[key_out][0,1,1] self.symmetries[key_out][0,1,1] = -self.symmetries[key_out][0,1,1]
self.symmetries[key_out][2,1,1] = -self.symmetries[key_out][2,1,1] self.symmetries[key_out][2,1,1] = -self.symmetries[key_out][2,1,1]
@ -297,15 +187,90 @@ class PPP:
# Exchange ghost cells and set boundary conditions # Exchange ghost cells and set boundary conditions
self.exchange_ghost_cells(key_out) self.exchange_ghost_cells(key_out)
self.impose_boundary_conditions(key_out) self.impose_boundary_conditions(key_out)
# Shift to pressure grid if desired
if on_pressure_grid:
self.shift_to_pressure_grid(key_out,key_out=key_out)
def allocate(self,key_grid,fill=None,pseudo=False,dtype=np.float64):
from .field import Field3d
ib = self.proc_grid[key_grid][0][self.ip]
ie = self.proc_grid[key_grid][1][self.ip]
jb = self.proc_grid[key_grid][2][self.jp]
je = self.proc_grid[key_grid][3][self.jp]
kb = self.proc_grid[key_grid][4][self.kp]
ke = self.proc_grid[key_grid][5][self.kp]
origin = (self.origin[key_grid][0]+(ib-1-self.nghx)*self.spacing[0],
self.origin[key_grid][1]+(jb-1-self.nghy)*self.spacing[1],
self.origin[key_grid][2]+(kb-1-self.nghz)*self.spacing[2])
dim = (ie-ib+1+2*self.nghx,
je-jb+1+2*self.nghy,
ke-kb+1+2*self.nghz)
return Field3d.allocate(dim,origin,self.spacing,fill=fill,pseudo=pseudo,dtype=dtype)
def qcriterion(self,key_out=None,from_pressure=False,keep_derivatives=False):
'''Computes Q-criterion'''
from .field import Field3d
if key_out is None:
key_out = 'Q'
if from_pressure:
assert 'p' in self.field, "Pressure field is not loaded."
self.copy('p',key_out,skip_data=True)
self.field[key_out] = 0.5*self.field['p'].laplacian(only_keep_interior=False)
else:
assert 'u' in self.field, "Velocity field is not loaded: u"
assert 'v' in self.field, "Velocity field is not loaded: v"
assert 'w' in self.field, "Velocity field is not loaded: w"
self.copy('p',key_out,skip_data=True,skip_symmetries=True)
self.init_symmetry(key_out)
self.field[key_out] = self.allocate('p',fill=0.0,dtype=self.field['u'].dtype())
keyx = ('x','y','z')
keyu = ('u','v','w')
for ii in range(3):
key_tmp = 'd'+keyu[ii]+'/d'+keyx[ii]
parprint(key_tmp)
self.differentiate(keyu[ii],axis=ii,key_out=key_tmp,on_pressure_grid=True)
self.field[key_out] += self.field[key_tmp]**2
if not keep_derivatives:
self.delete(key_tmp)
#
iip1 = (ii+1)%3
key_tmp1 = 'd'+keyu[ii]+'/d'+keyx[iip1]
key_tmp2 = 'd'+keyu[iip1]+'/d'+keyx[ii]
parprint(key_tmp1,key_tmp2)
self.differentiate(keyu[ii],axis=iip1,key_out=key_tmp1,on_pressure_grid=True)
self.differentiate(keyu[iip1],axis=ii,key_out=key_tmp2,on_pressure_grid=True)
self.field[key_out] += 2.0*self.field[key_tmp1]*self.field[key_tmp2]
if not keep_derivatives:
self.delete(key_tmp1)
self.delete(key_tmp2)
self.field[key_out] *= -0.5
# Exchange ghost cells and set boundary conditions
self.exchange_ghost_cells(key_out)
self.impose_boundary_conditions(key_out)
return
#def vorticity(self,axis=None,key_out=None,keep_derivatives=False,on_pressure_grid=True):
def multiply(self,key1,key2,key_out=None,on_pressure_grid=True):
if key_out is None:
key_out = key1
if not self.field[key1].has_same_grid(self.field[key2]) or on_pressure_grid:
self.shift_to_pressure_grid(key1,key_out)
self.shift_to_pressure_grid(key2,'tmp')
self.field[key_out] *= self.field['tmp']
self.delete('tmp')
else:
self.copy(key1,key_out)
self.field[key_out] *= self.field[key2]
return
def gaussian_filter(self,key,sigma,truncate=4.0,key_out=None,iterate=False): def gaussian_filter(self,key,sigma,truncate=4.0,key_out=None,iterate=False):
'''Applies a gaussian filter to a field as in-place operation. Sigma is the std of the filter in terms of grid width.''' '''Applies a gaussian filter to a field as in-place operation. Sigma is the std of the filter in terms of grid width.'''
import numpy as np import numpy as np
if key_out is None: if key_out is None:
key_out = key key_out = key
else: if key!=key_out:
self.origin[key_out] = self.origin[key].copy() self.copy(key,key_out,skip_data=True)
self.spacing[key_out] = self.spacing[key].copy()
# Compute radius of Gaussian filter # Compute radius of Gaussian filter
radius = self.field[key].gaussian_filter_radius(sigma,truncate=truncate) radius = self.field[key].gaussian_filter_radius(sigma,truncate=truncate)
if not iterate: if not iterate:
@ -324,23 +289,27 @@ class PPP:
assert all([radius[ii]>0 if sigma[ii]>0.0 else True for ii in range(3)]),\ assert all([radius[ii]>0 if sigma[ii]>0.0 else True for ii in range(3)]),\
"Iterative procedure leads to invalid stencil radius: "\ "Iterative procedure leads to invalid stencil radius: "\
"increase number of ghost cells. {}".format(radius) "increase number of ghost cells. {}".format(radius)
print('Iterations: {}, stencil radius: {}'.format(niter,radius)) parprint('Gaussian filter: iterations={}, stencil radius={}'.format(niter,radius))
for iiter in range(niter): for iiter in range(niter):
parprint('Iter #{:d}'.format(iiter))
# Filter field: if key_out is None, perform operation inplace # Filter field: if key_out is None, perform operation inplace
self.field[key_out] = self.field[key].gaussian_filter(sigma, self.field[key_out] = self.field[key].gaussian_filter(sigma,
truncate=truncate,border='constant',const_val=0.0) truncate=truncate,only_keep_interior=False)
# Exchange ghost cells and set boundary conditions # Exchange ghost cells and set boundary conditions
self.exchange_ghost_cells(key_out) self.exchange_ghost_cells(key_out)
self.impose_boundary_conditions(key_out) self.impose_boundary_conditions(key_out)
# Iterate inplace from now on # Iterate inplace from now on
key = key_out key = key_out
def broadcast(self,key,arg,operation): def broadcast(self,key,operation,arg,key_out=None):
'''Broadcasts an inplace operation involving a scalar or matrix on '''Broadcasts an operation involving a scalar or matrix on
the entire grid. If 'arg' is a matrix, it must be three-dimensional the entire grid. If 'arg' is a matrix, it must be three-dimensional
and its axes must be singular or of length nx/ny/nz.''' and its axes must be singular or of length nx/ny/nz.'''
import numpy as np import numpy as np
import operator import operator
# Broadcast argument
arg = self.comm.bcast(arg,root=0)
# Select operator
if operation in ('add','+'): if operation in ('add','+'):
op = operator.iadd op = operator.iadd
elif operation in ('subtract','sub','-'): elif operation in ('subtract','sub','-'):
@ -358,7 +327,7 @@ class PPP:
for axis in range(3): for axis in range(3):
if arg.shape[axis]==1: if arg.shape[axis]==1:
continue continue
elif arg.shape[axis]==self.numpoints(key,axis=axis): elif arg.shape[axis]==self.dim(key,axis=axis):
pos = self.position_from_rank(self.rank,external=False)[axis] pos = self.position_from_rank(self.rank,external=False)[axis]
sl_arg[axis] = slice( sl_arg[axis] = slice(
self.proc_grid[key][2*axis][pos]-1, self.proc_grid[key][2*axis][pos]-1,
@ -366,16 +335,24 @@ class PPP:
else: else:
raise ValueError("'arg' must either be singular or match global "\ raise ValueError("'arg' must either be singular or match global "\
"grid dimension. (axis={}: got {:d}, expected {:d}".format( "grid dimension. (axis={}: got {:d}, expected {:d}".format(
axis,arg.shape[axis],self.numpoints(key,axis=axis))) axis,arg.shape[axis],self.dim(key,axis=axis)))
# Only operate on interior and communcate ghosts later # Only operate on interior and communcate ghosts later
sl_int = tuple(slice(self.num_ghost[ii],-self.num_ghost[ii]) for ii in range(3)) sl_int = tuple(slice(self.num_ghost[ii],-self.num_ghost[ii]) for ii in range(3))
sl_arg = tuple(sl_arg) sl_arg = tuple(sl_arg)
op(self.field[key].data[sl_int],arg[sl_arg]) if key_out is None:
op(self.field[key].data[sl_int],arg[sl_arg])
else:
self.copy(key,key_out)
op(self.field[key_out].data[sl_int],arg[sl_arg])
# Exchange ghost cells and set boundary conditions # Exchange ghost cells and set boundary conditions
self.exchange_ghost_cells(key) self.exchange_ghost_cells(key)
self.impose_boundary_conditions(key) self.impose_boundary_conditions(key)
elif isinstance(arg,(int,float)): elif isinstance(arg,(int,float)):
op(self.field[key].data,arg) if key_out is None:
op(self.field[key].data,arg)
else:
self.copy(key,key_out)
op(self.field[key_out].data,arg)
return return
def integrate(self,key,integrate_axis,ufunc=None,ignore_nan=False,average=False): def integrate(self,key,integrate_axis,ufunc=None,ignore_nan=False,average=False):
@ -396,7 +373,7 @@ class PPP:
weights_local = np.array(weights_local) weights_local = np.array(weights_local)
# Simple implementation: all processor communicate directly to root # Simple implementation: all processor communicate directly to root
if self.rank==0: if self.rank==0:
dim_final = tuple(1 if integrate_axis[axis] else self.numpoints(key,axis=axis) for axis in range(3)) dim_final = tuple(1 if integrate_axis[axis] else self.dim(key,axis=axis) for axis in range(3))
# Allocate a nice spot of memory # Allocate a nice spot of memory
integ = np.zeros(dim_final,dtype=integ_local.dtype) integ = np.zeros(dim_final,dtype=integ_local.dtype)
if average: if average:
@ -432,47 +409,169 @@ class PPP:
self.comm.send(weights_local,dest=0,tag=2) self.comm.send(weights_local,dest=0,tag=2)
return None return None
def shift_to_pressure_grid(self,key,key_out=None):
from .field import Field3d
if key_out is None:
if key in ('u','v','w'):
raise ValueError("In-place shift of {'u','v','w'} is not permitted.")
key_out = key
# If pressure is loaded, use field information, else create a
# fake Field3d object so that we can use its methods.
field_ref = self.allocate('p',pseudo=True)
# Compute the relative shift and shift it
rel_shift = self.field[key].relative_shift(field_ref)
self.field[key_out] = self.field[key].shift_origin(rel_shift)
# Match number of gridpoints
sl = 3*[slice(None)]
for axis in range(3):
if not self.field[key_out].has_same_dim(field_ref,axis=axis):
sl[axis] = slice(0,field_ref.dim(axis=axis))
self.field[key_out].data = self.field[key_out].data[sl]
# Copy metadata of pressure grid
self.copy('p',key_out,skip_data=True,skip_symmetries=True)
self.symmetries[key_out] = self.symmetries[key].copy()
# Exchange ghost cells and set boundary conditions
self.exchange_ghost_cells(key_out)
self.impose_boundary_conditions(key_out)
def init_symmetry(self,key,mirror=(True,True)):
'''Sets the symmetries for ghost cells behind the wall'''
# Example: wall in y
# No-slip boundary (no mirror) free-slip boundary (mirror)
# u -> -u u -> u
# v -> -v v -> -v
# w -> -w w -> w
# p -> p p -> p
import numpy as np
self.symmetries[key] = np.zeros((3,3,3),dtype='i')
if not self.xperiodic:
if key=='u':
self.symmetries[key][0,1,1] = -1
self.symmetries[key][2,1,1] = -1
else:
self.symmetries[key][0,1,1] = 1 if mirror[0] else -1
self.symmetries[key][2,1,1] = 1 if mirror[1] else -1
if not self.yperiodic:
if key=='v':
self.symmetries[key][1,0,1] = -1
self.symmetries[key][1,2,1] = -1
else:
self.symmetries[key][1,0,1] = 1 if mirror[0] else -1
self.symmetries[key][1,2,1] = 1 if mirror[1] else -1
if not self.zperiodic:
if key=='w':
self.symmetries[key][1,1,0] = -1
self.symmetries[key][1,1,2] = -1
else:
self.symmetries[key][1,1,0] = 1 if mirror[0] else -1
self.symmetries[key][1,1,2] = 1 if mirror[1] else -1
def copy(self,key,key_out,skip_data=False,skip_symmetries=False):
'''Copies a field to a new key.'''
if key==key_out:
return
self.origin[key_out] = tuple(self.origin[key])
self.proc_grid[key_out] = self.proc_grid[key].copy()
if not skip_symmetries:
self.symmetries[key_out] = self.symmetries[key].copy()
if not skip_data:
self.field[key_out] = self.field[key].copy()
return
def delete(self,key):
'''Removes a field from memory.'''
assert key in self.field, "Field {} cannot be deleted, because it "\
"does not exist.".format(key)
import gc
del self.field[key]
del self.symmetries[key]
if key not in ('u','v','w','p','s'):
del self.origin[key]
del self.proc_grid[key]
gc.collect()
return
def merge(self,key,stride=(1,1,1)):
'''Returns the complete (possibly downsampled) field at rank 0.'''
return
def save_state(self,filename):
import pickle
fout = filename+'.{:05d}.pickle'.format(self.rank)
payload = (
self.num_ghost,
self.chunks_per_proc,
self.origin,
self.spacing,
self.periodicity,
self.bounds,
self.proc_grid,
self.nxp,
self.nyp,
self.nzp,
self.nghbr,
self.field,
self.symmetries)
with open(fout,"wb") as f:
pickle.dump(payload,f)
def to_vtk(self,key,no_ghost=False):
'''Returns the field (only its interior + some ghost cells for plotting)
as a vtk mesh.'''
return self._subfield_for_vtk(key,no_ghost).to_vtk()
def vtk_contour(self,key,val): def vtk_contour(self,key,val):
'''Compute isocontour for chunks.''' '''Compute isocontour for chunks.'''
if any([self.num_ghost[ii]>1 for ii in range(3)]): return self._subfield_for_vtk(key,False).vtk_contour(val)
idx_origin = tuple(self.num_ghost[ii]-1 for ii in range(3))
numpoints = self.chunk_size(axis=None)
return self.field[key].extract_subfield(
idx_origin,numpoints).vtk_contour(val)
else:
return self.field[key].vtk_contour(val)
return
def vtk_slice(self,key,normal,origin): def vtk_slice(self,key,normal,origin):
'''Extracts a plane from field.''' '''Extracts a plane from field.'''
if any([self.num_ghost[ii]>1 for ii in range(3)]): return self._subfield_for_vtk(key,False).vtk_slice(normal,origin)
idx_origin = tuple(self.num_ghost[ii]-1 for ii in range(3))
numpoints = tuple(self.field[key].numpoints[ii]-2*(self.num_ghost[ii]-1) def _subfield_for_vtk(self,key,no_ghost):
for ii in range(3)) num_ghost = np.array(self.num_ghost)
return self.field[key].extract_subfield( idx_origin = num_ghost
idx_origin,numpoints).vtk_slice(normal,origin) dim = np.array(self.field[key].dim(axis=None))
else: dim -= 2*num_ghost
return self.field[key].vtk_slice(normal,origin) if not no_ghost:
return idx_origin -= 1
dim += 1
if self.ip==self.nxp-1: dim[0]+=1
if self.jp==self.nyp-1: dim[1]+=1
if self.kp==self.nzp-1: dim[2]+=1
idx_origin = tuple(idx_origin)
dim = tuple(dim)
return self.field[key].extract_subfield(idx_origin,dim)
def rank_from_position(self,ip,jp,kp,external=False): def rank_from_position(self,ip,jp,kp,external=False):
if external: if external:
nyp,nzp = self.nyp_ext,self.nzp_ext nyp,nzp = self.nyp_ext,self.nzp_ext
else: else:
nyp,nzp = self.nyp,self.nzp nyp,nzp = self.nyp,self.nzp
return ip*nyp*nzp+jp*nzp+kp return self._rank_from_position(ip,jp,kp,nyp,nzp)
def position_from_rank(self,rank,external=False): def position_from_rank(self,rank,external=False):
if external: if external:
nyp,nzp = self.nyp_ext,self.nzp_ext nyp,nzp = self.nyp_ext,self.nzp_ext
else: else:
nyp,nzp = self.nyp,self.nzp nyp,nzp = self.nyp,self.nzp
return self._position_from_rank(rank,nyp,nzp)
@staticmethod
def _rank_from_position(ip,jp,kp,nyp,nzp):
return ip*nyp*nzp+jp*nzp+kp
@staticmethod
def _position_from_rank(rank,nyp,nzp):
ip = rank//(nyp*nzp) ip = rank//(nyp*nzp)
jp = (rank//nzp)%nyp jp = (rank//nzp)%nyp
kp = rank%nzp kp = rank%nzp
return (ip,jp,kp) return (ip,jp,kp)
def shifting_state(self,key,axis=None): def shifting_state(self,key,axis=None):
'''Returns the relative shift of first global grid point to domain
boundaries as either 1 (= +0.5h), 0 (= 0h) or -1 (= -0.5h).'''
if axis is None: if axis is None:
return tuple(self.shifting_state(key,axis=ii) for ii in range(3)) return tuple(self.shifting_state(key,axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
@ -483,13 +582,13 @@ class PPP:
if axis is None: if axis is None:
return tuple(self.chunk_size(key,axis=ii) for ii in range(3)) return tuple(self.chunk_size(key,axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
return self.field[key].numpoints[axis]-2*self.num_ghost[axis] return self.field[key].dim(axis=axis)-2*self.num_ghost[axis]
def numpoints(self,key,axis=None): def dim(self,key,axis=None):
'''Returns the total number of gridpoints across all processors '''Returns the total number of gridpoints across all processors
without ghost cells.''' without ghost cells.'''
if axis is None: if axis is None:
return tuple(self.numpoints(key,axis=ii) for ii in range(3)) return tuple(self.dim(key,axis=ii) for ii in range(3))
assert axis<3, "'axis' must be one of 0,1,2." assert axis<3, "'axis' must be one of 0,1,2."
return self.proc_grid[key][2*axis+1][-1] return self.proc_grid[key][2*axis+1][-1]
@ -600,7 +699,7 @@ class PPP:
recvbuf = np.zeros( recvbuf = np.zeros(
(ii_dst.stop-ii_dst.start, (ii_dst.stop-ii_dst.start,
jj_dst.stop-jj_dst.start, jj_dst.stop-jj_dst.start,
kk_dst.stop-kk_dst.start),dtype=self.precision) kk_dst.stop-kk_dst.start),dtype=self.field[key].data.dtype)
reqrecv = self.comm.Irecv(recvbuf,source=rank_src,tag=tag) reqrecv = self.comm.Irecv(recvbuf,source=rank_src,tag=tag)
# [recv] wait for data to be received # [recv] wait for data to be received
if reqrecv is not None: if reqrecv is not None:
@ -625,7 +724,7 @@ class PPP:
for axis in range(3): for axis in range(3):
if positionBd[axis]==-1: # lower boundary if positionBd[axis]==-1: # lower boundary
# index of first point within the domain # index of first point within the domain
idx = self.field[key].nearest_gridpoint(self.bounds[2*axis],axis=axis,lower=True)+1 idx = self.field[key].nearest_gridpoint(self.bounds[2*axis],axis=axis,lower=True)+1
# distance first point to wall: should be either 0 or dx/2 # distance first point to wall: should be either 0 or dx/2
dist = np.abs(self.field[key].coordinate(idx,axis=axis)-self.bounds[2*axis]) dist = np.abs(self.field[key].coordinate(idx,axis=axis)-self.bounds[2*axis])
if dist>=0.25*self.spacing[axis]: # no point on boundary if dist>=0.25*self.spacing[axis]: # no point on boundary
@ -637,19 +736,123 @@ class PPP:
break break
elif positionBd[axis]==1: # upper boundary elif positionBd[axis]==1: # upper boundary
# index of last point within the domain # index of last point within the domain
idx = self.field[key].nearest_gridpoint(self.bounds[2*axis+1],axis=axis,lower=True) idx = self.field[key].nearest_gridpoint(self.bounds[2*axis+1],axis=axis,lower=True)
# distance last point to wall: should be either 0 or -dx/2 # distance last point to wall: should be either 0 or -dx/2
dist = np.abs(self.field[key].coordinate(idx,axis=axis)-self.bounds[2*axis+1]) dist = np.abs(self.field[key].coordinate(idx,axis=axis)-self.bounds[2*axis+1])
if dist>=0.25*self.spacing[axis]: # no point on boundary if dist>=0.25*self.spacing[axis]: # no point on boundary
sl_dst[axis] = slice(idx+1,self.field[key].numpoints[axis],1) sl_dst[axis] = slice(idx+1,self.field[key].dim(axis=axis),1)
sl_src[axis] = slice(idx,2*idx+1-self.field[key].numpoints[axis],-1) sl_src[axis] = slice(idx,2*idx+1-self.field[key].dim(axis=axis),-1)
else: # point on boundary else: # point on boundary
sl_dst[axis] = slice(idx+1,self.field[key].numpoints[axis],1) sl_dst[axis] = slice(idx+1,self.field[key].dim(axis=axis),1)
sl_src[axis] = slice(idx-1,2*idx-self.field[key].numpoints[axis],-1) sl_src[axis] = slice(idx-1,2*idx-self.field[key].dim(axis=axis),-1)
break break
# print(sl_src,sl_dst,self.field[key].dim())
self.field[key].data[tuple(sl_dst)] = self.symmetries[key][inghbr,jnghbr,knghbr]*\ self.field[key].data[tuple(sl_dst)] = self.symmetries[key][inghbr,jnghbr,knghbr]*\
self.field[key].data[tuple(sl_src)] self.field[key].data[tuple(sl_src)]
@staticmethod
def _construct_procgrid(comm,proc,chunks_per_proc,num_ghost,periodicity):
import numpy as np
rank = comm.Get_rank()
nxcpp,nycpp,nzcpp = chunks_per_proc
nghx,nghy,nghz = num_ghost
xperiodic,yperiodic,zperiodic = periodicity
# Assert proc
assert all(k in proc for k in ('u','v','w','p','s')), "'proc' must be a dictionary with "\
"keys 'u','v','w','p','s'"
for key in proc:
assert len(proc[key])==6, "Entries of 'proc' must have length of 6."
proc_grid_ext = proc
# Initialize chunks per processor
# Verify that this processor grid can be redistributed onto the requested processor layout
nxp_ext = len(proc_grid_ext['u'][0])
nyp_ext = len(proc_grid_ext['u'][2])
nzp_ext = len(proc_grid_ext['u'][4])
nproc_ext = nxp_ext*nyp_ext*nzp_ext
#
assert nxp_ext%nxcpp==0, "Number of processors must be divisible by the number "\
"of processors per process. (nxp_ext={}, nxcpp={})".format(
nxp_ext,nxcpp)
assert nyp_ext%nycpp==0, "Number of processors must be divisible by the number "\
"of processors per process. (nyp_ext={}, nycpp={})".format(
nyp_ext,nycpp)
assert nzp_ext%nzcpp==0, "Number of processors must be divisible by the number "\
"of processors per process. (nzp_ext={}, nzcpp={})".format(
nzp_ext,nzcpp)
# Determine the new processor layout and verify total number of MPI processes
nxp = nxp_ext//nxcpp
nyp = nyp_ext//nycpp
nzp = nzp_ext//nzcpp
nproc = nxp*nyp*nzp
assert nproc==comm.Get_size(), "Number of MPI processes does not match the requested "\
"processor layout. (MPI procs: {}, required procs: {})".format(
comm.Get_size(),nproc)
# Construct internal processor grid
proc_grid = {}
for key in proc_grid_ext:
proc_grid[key] = [None]*6
proc_grid[key][0] = proc_grid_ext[key][0][0::nxcpp]
proc_grid[key][1] = proc_grid_ext[key][1][nxcpp-1::nxcpp]
proc_grid[key][2] = proc_grid_ext[key][2][0::nycpp]
proc_grid[key][3] = proc_grid_ext[key][3][nycpp-1::nycpp]
proc_grid[key][4] = proc_grid_ext[key][4][0::nzcpp]
proc_grid[key][5] = proc_grid_ext[key][5][nzcpp-1::nzcpp]
# Get position in processor grid
ip,jp,kp = PPP._position_from_rank(rank,nyp,nzp)
# Determine local grid indices and size
for key in proc_grid:
nxl = proc_grid[key][1][ip]-proc_grid[key][0][ip]+1
nyl = proc_grid[key][3][jp]-proc_grid[key][2][jp]+1
nzl = proc_grid[key][5][kp]-proc_grid[key][4][kp]+1
# Verify that local grid size is not smaller than ghost cell size
assert (nxl>=nghx and nyl>=nghy and nzl>=nghz),\
"Local grid size must be greater than number "\
"of ghost cells in each direction!"
# Initialize neighbor array
nghbr = np.empty((3,3,3),dtype='int')
# wrap-around x
ipl = ip-1
if ipl<0:
if xperiodic: ipl = nxp-1
else: ipl = -1
ipr = ip+1
if ipr>nxp-1:
if xperiodic: ipr = 0
else: ipr = -1
inghbr = (ipl,ip,ipr)
# wrap-around y
jpl = jp-1
if jpl<0:
if yperiodic: jpl = nyp-1
else: jpl = -1
jpr = jp+1
if jpr>nyp-1:
if yperiodic: jpr = 0
else: jpr = -1
jnghbr = (jpl,jp,jpr)
# wrap-around z
kpl = kp-1
if kpl<0:
if zperiodic: kpl = nzp-1
else: kpl = -1
kpr = kp+1
if kpr>nzp-1:
if zperiodic: kpr = 0
else: kpr = -1
knghbr = (kpl,kp,kpr)
# Construct array of neighbors
for ip in range(3):
for jp in range(3):
for kp in range(3):
# Assign rank to neighbor array
if inghbr[ip]<0 or jnghbr[jp]<0 or knghbr[kp]<0:
nghbr[ip,jp,kp] = -1
else:
nghbr[ip,jp,kp] = PPP._rank_from_position(inghbr[ip],jnghbr[jp],knghbr[kp],nyp,nzp)
return (proc_grid,nxp,nyp,nzp,
proc_grid_ext,nxp_ext,nyp_ext,nzp_ext,
nghbr)
def _baton_wait(self,batch_size,tag=420): def _baton_wait(self,batch_size,tag=420):
'''Block execution until an empty message from rank-batch_wait '''Block execution until an empty message from rank-batch_wait
is received (issued by _baton_pass)''' is received (issued by _baton_pass)'''

View File

@ -1,37 +1,36 @@
import numpy as np
class Particles: class Particles:
def __init__(self,num,time,attr,period): def __init__(self,num,time,attr,period):
'''Initialize a Particles object. '''Initialize a Particles object.
num number of particles num number of particles
time time value time time value
attr dictionary of numpy arrays with length equal to num attr dictionary of np arrays with length equal to num
period the domain periods period the domain periods
''' '''
import numpy
assert 'id' in attr, "Need attribute 'id' to initialize Particles" assert 'id' in attr, "Need attribute 'id' to initialize Particles"
assert 'x' in attr, "Need attribute 'x' to initialize Particles" assert 'x' in attr, "Need attribute 'x' to initialize Particles"
assert 'y' in attr, "Need attribute 'y' to initialize Particles" assert 'y' in attr, "Need attribute 'y' to initialize Particles"
assert 'z' in attr, "Need attribute 'z' to initialize Particles" assert 'z' in attr, "Need attribute 'z' to initialize Particles"
assert isinstance(period,(tuple,list)) and len(period)==3, "'period' must be tuple/list with length 3" assert isinstance(period,(tuple,list)) and len(period)==3, "'period' must be tuple/list with length 3"
self.num = num self.num = num
if isinstance(time,(tuple,list,numpy.ndarray)): if isinstance(time,(tuple,list,np.ndarray)):
assert len(time)==1, "'time' must be a scalar value." assert len(time)==1, "'time' must be a scalar value."
time = time[0] time = time[0]
self.time = time self.time = time
self.attr = {} self.attr = {}
sortidx = attr['id'].argsort() sortidx = attr['id'].argsort()
for key in attr: for key in attr:
assert attr[key].ndim==1, "Attributes must be a one-dimensional numpy array." assert attr[key].ndim==1, "Attributes must be a one-dimensional np array."
assert attr[key].shape[0]==num, "Attributes must be of length 'num'." assert attr[key].shape[0]==num, "Attributes must be of length 'num'."
if key=='id': if key=='id':
self.add_attribute(key,attr[key][sortidx].astype('int')) self.add_attribute(key,attr[key][sortidx].astype('int'))
else: else:
self.add_attribute(key,attr[key][sortidx]) self.add_attribute(key,attr[key][sortidx])
self.period = tuple(period) self.period = tuple(period)
self.frame_velocity = numpy.zeros(3) self.frame_velocity = np.zeros(3)
return return
@classmethod @classmethod
def from_array(cls,pp,col,time,period,select_col=None): def from_array(cls,pp,col,time,period,select_col=None):
import numpy
assert 'id' in col, "Need column 'id' to initialize Particles" assert 'id' in col, "Need column 'id' to initialize Particles"
assert 'x' in col, "Need column 'x' to initialize Particles" assert 'x' in col, "Need column 'x' to initialize Particles"
assert 'y' in col, "Need column 'y' to initialize Particles" assert 'y' in col, "Need column 'y' to initialize Particles"
@ -39,7 +38,7 @@ class Particles:
assert pp.ndim==2 or pp.shape[2]==1, "'pp' cannot be a time series." assert pp.ndim==2 or pp.shape[2]==1, "'pp' cannot be a time series."
num = pp.shape[1] num = pp.shape[1]
if pp.ndim==2: if pp.ndim==2:
pp = numpy.expand_dims(pp,axis=2) pp = np.expand_dims(pp,axis=2)
if select_col is not None: if select_col is not None:
for scol in select_col: for scol in select_col:
assert scol in col, "Selected column '{}' not found in 'col'.".format(scol) assert scol in col, "Selected column '{}' not found in 'col'.".format(scol)
@ -78,21 +77,19 @@ class Particles:
attr[key] = self.attr[key].copy() attr[key] = self.attr[key].copy()
return Particles(self.num,self.time,attr,self.period) return Particles(self.num,self.time,attr,self.period)
def add_attribute(self,key,val): def add_attribute(self,key,val):
import numpy if isinstance(val,(tuple,list,np.ndarray)):
if isinstance(val,(tuple,list,numpy.ndarray)):
assert len(val)==self.num and val.ndim==1, "Invalid 'val'." assert len(val)==self.num and val.ndim==1, "Invalid 'val'."
self.attr[key] = numpy.array(val) self.attr[key] = np.array(val)
else: else:
self.attr[key] = numpy.full(self.num,val) self.attr[key] = np.full(self.num,val)
return return
def del_attribute(self,key): def del_attribute(self,key):
del self.attr[key] del self.attr[key]
def get_attribute(self,key): def get_attribute(self,key):
return self.attr[key] return self.attr[key]
def get_position(self,axis=None): def get_position(self,axis=None):
import numpy
if axis is None: if axis is None:
return numpy.vstack([self.attr[key].copy() for key in ('x','y','z')]) return np.vstack([self.attr[key].copy() for key in ('x','y','z')])
else: else:
assert axis<3, "'axis' must be smaller than 3." assert axis<3, "'axis' must be smaller than 3."
key = ('x','y','z')[axis] key = ('x','y','z')[axis]
@ -113,7 +110,8 @@ class Particles:
valdiff = val-self.frame_velocity[axis] valdiff = val-self.frame_velocity[axis]
self.translate(-self.time*valdiff,axis=axis) self.translate(-self.time*valdiff,axis=axis)
key = ('u','v','w')[axis] key = ('u','v','w')[axis]
self.attr[key] -= valdiff if self.has_attribute(key):
self.attr[key] -= valdiff
self.frame_velocity[axis] = val self.frame_velocity[axis] = val
return return
def enforce_periodicity(self,axis=None): def enforce_periodicity(self,axis=None):
@ -127,7 +125,6 @@ class Particles:
return return
def to_vtk(self,deep=False): def to_vtk(self,deep=False):
import pyvista as pv import pyvista as pv
import numpy as np
position = np.vstack([self.attr[key] for key in ('x','y','z')]).transpose() position = np.vstack([self.attr[key] for key in ('x','y','z')]).transpose()
mesh = pv.PolyData(position,deep=deep) mesh = pv.PolyData(position,deep=deep)
for key in self.attr: for key in self.attr:
@ -143,12 +140,11 @@ class Particles:
class Trajectories: class Trajectories:
def __init__(self,part,unraveled=False,copy_particles=False): def __init__(self,part,unraveled=False,copy_particles=False):
import numpy
self.numtime = 1 self.numtime = 1
self.numpart = part.num self.numpart = part.num
self.copy_particles = copy_particles self.copy_particles = copy_particles
# Attributes are first stored as a list of view to instances of # Attributes are first stored as a list of view to instances of
# Particles. Later they will be concatenated into numpy array # Particles. Later they will be concatenated into np array
# and the attributes of Particles will be views. # and the attributes of Particles will be views.
self.attr = {} self.attr = {}
self.part = [] self.part = []
@ -161,7 +157,8 @@ class Trajectories:
self.attr[key] = [self.part[0].attr[key].view()] self.attr[key] = [self.part[0].attr[key].view()]
self.period = self.part[0].period self.period = self.part[0].period
self.unraveled = unraveled self.unraveled = unraveled
self.frame_velocity = numpy.zeros(3) self.frame_velocity = np.zeros(3)
def __str__(self): def __str__(self):
str = 'Trajectories with\n' str = 'Trajectories with\n'
str+= ' time steps: {:d}\n'.format(self.numtime) str+= ' time steps: {:d}\n'.format(self.numtime)
@ -173,6 +170,7 @@ class Trajectories:
str+= ' frame velocity: {}\n'.format(self.frame_velocity) str+= ' frame velocity: {}\n'.format(self.frame_velocity)
str+= ' unraveled: {}'.format(self.unraveled) str+= ' unraveled: {}'.format(self.unraveled)
return str return str
def __iadd__(self,other): def __iadd__(self,other):
if isinstance(other,Trajectories): if isinstance(other,Trajectories):
self.add_trajectories(other) self.add_trajectories(other)
@ -181,6 +179,7 @@ class Trajectories:
else: else:
raise TypeError('Cannot add type {}'.format(type(other))) raise TypeError('Cannot add type {}'.format(type(other)))
return self return self
def __getitem__(self,val): def __getitem__(self,val):
assert isinstance(val,tuple) and len(val)==2, "Trajectories must be indexed by [part,time]." assert isinstance(val,tuple) and len(val)==2, "Trajectories must be indexed by [part,time]."
sl = [] sl = []
@ -194,6 +193,7 @@ class Trajectories:
else: else:
raise TypeError("Trajectories can only be sliced by slice objects or integers.") raise TypeError("Trajectories can only be sliced by slice objects or integers.")
return self._slice(slice_part=sl[0],slice_time=sl[1]) return self._slice(slice_part=sl[0],slice_time=sl[1])
@classmethod @classmethod
def from_mat(cls,file,unraveled=False,select_col=None): def from_mat(cls,file,unraveled=False,select_col=None):
from .helper import load_mat from .helper import load_mat
@ -206,6 +206,7 @@ class Trajectories:
for key in col: for key in col:
col[key]-=1 col[key]-=1
return cls.from_array(pp,col,time,period,unraveled=unraveled,select_col=select_col) return cls.from_array(pp,col,time,period,unraveled=unraveled,select_col=select_col)
@classmethod @classmethod
def from_array(cls,pp,col,time,period,unraveled=False,select_col=None): def from_array(cls,pp,col,time,period,unraveled=False,select_col=None):
ntime = len(time) ntime = len(time)
@ -213,6 +214,7 @@ class Trajectories:
for ii in range(1,ntime): for ii in range(1,ntime):
traj += Particles.from_array(pp[:,:,ii],col,time[ii],period,select_col=select_col) traj += Particles.from_array(pp[:,:,ii],col,time[ii],period,select_col=select_col)
return traj return traj
def _slice(self,slice_part=slice(None),slice_time=slice(None)): def _slice(self,slice_part=slice(None),slice_time=slice(None)):
assert isinstance(slice_part,slice), "'slice_part' must be a slice object." assert isinstance(slice_part,slice), "'slice_part' must be a slice object."
assert isinstance(slice_time,slice), "'slice_time' must be a slice object." assert isinstance(slice_time,slice), "'slice_time' must be a slice object."
@ -225,8 +227,8 @@ class Trajectories:
traj += Trajectories(part[slice_part]) traj += Trajectories(part[slice_part])
traj.frame_velocity = self.frame_velocity traj.frame_velocity = self.frame_velocity
return traj return traj
def add_particles(self,part): def add_particles(self,part):
import numpy
# Verify part # Verify part
assert part.time>self.part[-1].time, "Time steps must be added in monotonically increasing order." assert part.time>self.part[-1].time, "Time steps must be added in monotonically increasing order."
assert part.num==self.numpart, "Number of particles is different from previous time steps." assert part.num==self.numpart, "Number of particles is different from previous time steps."
@ -246,6 +248,7 @@ class Trajectories:
self.attr[key].append(self.part[-1].attr[key].view()) self.attr[key].append(self.part[-1].attr[key].view())
self.numtime += 1 self.numtime += 1
return return
def add_trajectories(self,traj): def add_trajectories(self,traj):
# Verify traj # Verify traj
assert traj.part[0].time>self.part[-1].time, "Time steps must be added in monotonically increasing order." assert traj.part[0].time>self.part[-1].time, "Time steps must be added in monotonically increasing order."
@ -267,26 +270,26 @@ class Trajectories:
self.attr[key].append(self.part[-traj.numtime+ii].attr[key].view()) self.attr[key].append(self.part[-traj.numtime+ii].attr[key].view())
self.numtime += traj.numtime self.numtime += traj.numtime
return return
def get_time(self): def get_time(self):
import numpy time = np.zeros(self.numtime)
time = numpy.zeros(self.numtime)
for ii,part in enumerate(self.part): for ii,part in enumerate(self.part):
time[ii] = part.time time[ii] = part.time
return time return time
def get_trajectories(self,slice_part=slice(None),slice_time=slice(None)): def get_trajectories(self,slice_part=slice(None),slice_time=slice(None)):
'''Get (x,y,z) trajectories in numpy array of dimension (3,npart,ntime).''' '''Get (x,y,z) trajectories in np array of dimension (3,npart,ntime).'''
import numpy
assert isinstance(slice_part,slice), "'slice_part' must be a slice." assert isinstance(slice_part,slice), "'slice_part' must be a slice."
assert isinstance(slice_time,slice), "'slice_time' must be a slice." assert isinstance(slice_time,slice), "'slice_time' must be a slice."
self._make_data_array() self._make_data_array()
return numpy.stack([ return np.stack([
self.attr['x'][slice_part,slice_time], self.attr['x'][slice_part,slice_time],
self.attr['y'][slice_part,slice_time], self.attr['y'][slice_part,slice_time],
self.attr['z'][slice_part,slice_time]],axis=0) self.attr['z'][slice_part,slice_time]],axis=0)
def get_trajectories_segmented(self,slice_part=slice(None),slice_time=slice(None),restore_ravel_state=True): def get_trajectories_segmented(self,slice_part=slice(None),slice_time=slice(None),restore_ravel_state=True):
'''Get (x,y,z) segments of trajectories as a tuple (len: npart) of '''Get (x,y,z) segments of trajectories as a tuple (len: npart) of
lists (len: nsegments) of numpy arrays (shape: 3 X ntime of the segment)''' lists (len: nsegments) of np arrays (shape: 3 X ntime of the segment)'''
import numpy
assert isinstance(slice_part,slice), "'slice_part' must be a slice." assert isinstance(slice_part,slice), "'slice_part' must be a slice."
assert isinstance(slice_time,slice), "'slice_time' must be a slice." assert isinstance(slice_time,slice), "'slice_time' must be a slice."
was_unraveled = self.unraveled was_unraveled = self.unraveled
@ -296,21 +299,21 @@ class Trajectories:
ntime = xyzpath.shape[2] ntime = xyzpath.shape[2]
# Initialize output and helper arrays # Initialize output and helper arrays
out = tuple([] for ii in range(npart)) out = tuple([] for ii in range(npart))
lastJump = numpy.zeros(npart,dtype=numpy.uint) lastJump = np.zeros(npart,dtype=np.uint)
lastPeriod = numpy.zeros((3,npart),dtype=numpy.int) lastPeriod = np.zeros((3,npart),dtype=np.int)
for axis in range(3): for axis in range(3):
if self.period[axis] is not None: if self.period[axis] is not None:
lastPeriod[axis,:] = numpy.floor_divide(xyzpath[axis,:,0],self.period[axis]) lastPeriod[axis,:] = np.floor_divide(xyzpath[axis,:,0],self.period[axis])
# Construct output tuple # Construct output tuple
for itime in range(1,ntime+1): for itime in range(1,ntime+1):
thisPeriod = numpy.zeros((3,npart),dtype=int) thisPeriod = np.zeros((3,npart),dtype=int)
if itime==ntime: if itime==ntime:
hasJumped = numpy.ones(npart,dtype=bool) hasJumped = np.ones(npart,dtype=bool)
else: else:
for axis in range(3): for axis in range(3):
if self.period[axis] is not None: if self.period[axis] is not None:
thisPeriod[axis,:] = numpy.floor_divide(xyzpath[axis,:,itime],self.period[axis]) thisPeriod[axis,:] = np.floor_divide(xyzpath[axis,:,itime],self.period[axis])
hasJumped = numpy.any(thisPeriod!=lastPeriod,axis=0) hasJumped = np.any(thisPeriod!=lastPeriod,axis=0)
for ipart in range(npart): for ipart in range(npart):
if hasJumped[ipart]: if hasJumped[ipart]:
sl = slice(lastJump[ipart],itime) sl = slice(lastJump[ipart],itime)
@ -325,8 +328,20 @@ class Trajectories:
if restore_ravel_state and not was_unraveled: if restore_ravel_state and not was_unraveled:
self.enforce_periodicity() self.enforce_periodicity()
return out return out
def slice_time(self,time_beg=None,time_end=None): def slice_time(self,time_beg=None,time_end=None):
return time = self.get_time()
if time_beg is not None:
idx_beg = np.searchsorted(time,time_beg,side='left')
else:
idx_beg = None
if time_end is not None:
idx_end = np.searchsorted(time,time_end,side='left')
else:
idx_end = None
sl = slice(idx_beg,idx_end+1)
return self._slice(slice_time=sl)
def set_frame_velocity(self,val,axis=0): def set_frame_velocity(self,val,axis=0):
assert axis<3, "'axis' must be smaller than 3." assert axis<3, "'axis' must be smaller than 3."
for part in self.part: for part in self.part:
@ -335,8 +350,8 @@ class Trajectories:
if not self.unraveled: if not self.unraveled:
self.enforce_periodicity() self.enforce_periodicity()
return return
def unravel(self): def unravel(self):
import numpy
if self.unraveled: return if self.unraveled: return
#raise NotImplementedError('Implemented but untested!') #raise NotImplementedError('Implemented but untested!')
self._make_data_array() self._make_data_array()
@ -345,48 +360,56 @@ class Trajectories:
key = ('x','y','z')[axis] key = ('x','y','z')[axis]
#posdiff = (self.part[ii].attr[key]-self.part[ii-1].attr[key]).squeeze() #posdiff = (self.part[ii].attr[key]-self.part[ii-1].attr[key]).squeeze()
posdiff = self.attr[key][:,1:]-self.attr[key][:,0:-1] posdiff = self.attr[key][:,1:]-self.attr[key][:,0:-1]
coeff = -numpy.sign(posdiff)*(numpy.abs(posdiff)>0.5*self.period[axis]) coeff = -np.sign(posdiff)*(np.abs(posdiff)>0.5*self.period[axis])
coeff = numpy.cumsum(coeff,axis=1) coeff = np.cumsum(coeff,axis=1)
self.attr[key][:,1:] += coeff*self.period[axis] self.attr[key][:,1:] += coeff*self.period[axis]
self.unraveled = True self.unraveled = True
return return
def enforce_periodicity(self): def enforce_periodicity(self):
for part in self.part: for part in self.part:
part.enforce_periodicity(axis=None) part.enforce_periodicity(axis=None)
self.unraveled = False self.unraveled = False
return return
def to_vtk(self):
import pyvista as pv
mesh = pv.PolyData()
for part in self.get_trajectories_segmented():
for seg in part:
mesh += pv.helpers.lines_from_points(seg.transpose())
return mesh
def _make_data_array(self): def _make_data_array(self):
'''Transforms self.attr[key] to a numpy array of dimension npart X ntime '''Transforms self.attr[key] to a np array of dimension npart X ntime
and updates self.part[itime].attr[key] to views on self.attr[key][:,itime].''' and updates self.part[itime].attr[key] to views on self.attr[key][:,itime].'''
import numpy
#print('DEBUG: _make_data_array') #print('DEBUG: _make_data_array')
if not self._is_data_list: if not self._is_data_list:
return return
#print([x.shape for x in self.attr['x']]) #print([x.shape for x in self.attr['x']])
for key in self.attr: for key in self.attr:
self.attr[key] = numpy.stack(self.attr[key],axis=1) self.attr[key] = np.stack(self.attr[key],axis=1)
for ii in range(0,self.numtime): for ii in range(0,self.numtime):
self.part[ii].attr[key] = self.attr[key][:,ii] self.part[ii].attr[key] = self.attr[key][:,ii]
#print(self.attr['x'].shape) #print(self.attr['x'].shape)
self._is_data_list = False self._is_data_list = False
return return
def _make_data_list(self): def _make_data_list(self):
'''Transforms self.attr[key] to a list of length ntime of numpy arrays of '''Transforms self.attr[key] to a list of length ntime of np arrays of
dimension npart and updates self.part[itime].attr[key] to views on self.attr[key][itime].''' dimension npart and updates self.part[itime].attr[key] to views on self.attr[key][itime].'''
import numpy
#print('DEBUG: _make_data_list') #print('DEBUG: _make_data_list')
if self._is_data_list: if self._is_data_list:
return return
#print(self.attr['x'].shape) #print(self.attr['x'].shape)
for key in self.attr: for key in self.attr:
self.attr[key] = [numpy.squeeze(x) for x in numpy.split(self.attr[key],self.numtime,axis=1)] self.attr[key] = [np.squeeze(x) for x in np.split(self.attr[key],self.numtime,axis=1)]
for ii in range(0,self.numtime): for ii in range(0,self.numtime):
self.part[ii].attr[key] = self.attr[key][ii].view() self.part[ii].attr[key] = self.attr[key][ii].view()
#print([x.shape for x in self.attr['x']]) #print([x.shape for x in self.attr['x']])
self._is_data_list = True self._is_data_list = True
return return
def sort(pp,col): def sort(pp,col):
ncol,npart,ntime = pp.shape ncol,npart,ntime = pp.shape
assert('id' in col) assert('id' in col)