its getting better

This commit is contained in:
Michael Krayer 2021-08-18 02:24:20 +02:00
parent 16e67ad666
commit e95b7dc74a
1 changed files with 144 additions and 173 deletions

317
field.py
View File

@ -657,8 +657,8 @@ class Features3d:
assert len(periodicity)==3, "'periodicity' must be of length 3" assert len(periodicity)==3, "'periodicity' must be of length 3"
assert isinstance(input,np.ndarray), "'input' must be numpy.ndarray." assert isinstance(input,np.ndarray), "'input' must be numpy.ndarray."
# Assign basic properties to class variables # Assign basic properties to class variables
self.origin = np.array(origin,dtype=np.float)
self.spacing = np.array(spacing,dtype=np.float) self.spacing = np.array(spacing,dtype=np.float)
self.origin = np.array(origin,dtype=np.float)-self.spacing
self.periodicity = tuple(bool(x) for x in periodicity) self.periodicity = tuple(bool(x) for x in periodicity)
# If regions are supposed to be inverted, i.e. the interior consists of values # If regions are supposed to be inverted, i.e. the interior consists of values
# smaller than the threshold instead of larger, change the sign of the array. # smaller than the threshold instead of larger, change the sign of the array.
@ -667,13 +667,18 @@ class Features3d:
# '_input' is the array which is to be triangulated. Here one trailing ghost cell is # '_input' is the array which is to be triangulated. Here one trailing ghost cell is
# required to get closed surfaces. The data will always be copied since it probably # required to get closed surfaces. The data will always be copied since it probably
# needs to be copied for vtk anyway (needs FORTRAN contiguous array) and this way # needs to be copied for vtk anyway (needs FORTRAN contiguous array) and this way
# there will never be side effects on the input data. # there will never be side effects on the input data. The array is padded with
# a very high value (cannot use NaN or Inf for vtk) in order to close the contour at
# the boundaries.
if has_ghost: if has_ghost:
self._input = sign_invert*input self.dimensions = input.shape+np.array((2,2,2))
self._input = np.full(self.dimensions,-1e30,dtype=input.dtype,order='F')
self._input[1:-1,1:-1,1:-1] = sign_invert*input
else: else:
pw = tuple((0,1) if x else (0,0) for x in periodicity) pw = tuple((0,1) if x else (0,0) for x in periodicity)
self._input = np.pad(sign_invert*input,pw,mode='wrap') self.dimensions = input.shape+np.array((2,2,2))+np.array([sum(x) for x in pw])
self.dimensions = np.array(self._input.shape,dtype=np.int) self._input = np.full(self.dimensions,-1e30,dtype=input.dtype,order='F')
self._input[1:-1,1:-1,1:-1] = np.pad(sign_invert*input,pw,mode='wrap')
# Triangulate # Triangulate
self.triangulate(contour_method=contour_method, self.triangulate(contour_method=contour_method,
cellvol_normal_component=cellvol_normal_component, cellvol_normal_component=cellvol_normal_component,
@ -701,6 +706,7 @@ class Features3d:
import vtk import vtk
from scipy import ndimage, spatial from scipy import ndimage, spatial
from scipy.spatial import KDTree from scipy.spatial import KDTree
from numba import jit
from time import time from time import time
# Check if '_input' is available: might have been dropped after initialization # Check if '_input' is available: might have been dropped after initialization
assert self._input is not None, "'_input' not available. Initialize object with keep_input=True flag." assert self._input is not None, "'_input' not available. Initialize object with keep_input=True flag."
@ -710,169 +716,113 @@ class Features3d:
datavtk.origin = self.origin datavtk.origin = self.origin
datavtk.spacing = self.spacing datavtk.spacing = self.spacing
datavtk.point_arrays['data'] = self._input.ravel('F') datavtk.point_arrays['data'] = self._input.ravel('F')
# Compute full contour: ensure we only have triangles to efficiently extract points # Compute full contour: due to the padding, the contour will result in closed object.
# later on. # This is necessary for proper inside/outside checks and volume computation. However,
# for surface area computation, the boundary faces have to be removed. Therefore, we
# will isolate them and store them in an extra array.
if report: print('[Features3d.triangulate] computing isocontour using {}...'.format(contour_method)) if report: print('[Features3d.triangulate] computing isocontour using {}...'.format(contour_method))
contour = datavtk.contour([self._threshold],method=contour_method,compute_scalars=False,compute_gradients=True) contour = datavtk.contour([self._threshold],method=contour_method,compute_scalars=False,compute_gradients=True)
assert contour.is_all_triangles(), "Contouring produced non-triangle cells." assert contour.is_all_triangles(), "Contouring produced non-triangle cells."
# Compute contour on boundaries: this is necessary for inside/outside checks and proper points = contour.points
# volume computation for overlapping objects faces = contour.faces.reshape(-1,4)
t__ = time() # Python for loops are horribly slow, but for loops are the easiest way to perform
self._faces_bd = [] # some of the necessary check. Therefore let's define some functions to be jit compiled
self._points_bd = [] # by numba to speed things up.
offset_pts = contour.n_points @jit(nopython=True)
print(offset_pts) def __get_boundary_faces(faces,points,bounds,tolerance):
assert faces.shape[1]==4
assert points.shape[1]==3
assert len(bounds)==6
assert len(tolerance)==3
ncells = faces.shape[0]
output = np.full((ncells,),-1,dtype=np.int8)
for ii in range(ncells):
for jj in range(3):
if (points[faces[ii,1],jj]<bounds[2*jj]+tolerance[jj] and
points[faces[ii,2],jj]<bounds[2*jj]+tolerance[jj] and
points[faces[ii,3],jj]<bounds[2*jj]+tolerance[jj]):
output[ii] = 2*jj
elif (points[faces[ii,1],jj]>bounds[2*jj+1]-tolerance[jj] and
points[faces[ii,2],jj]>bounds[2*jj+1]-tolerance[jj] and
points[faces[ii,3],jj]>bounds[2*jj+1]-tolerance[jj]):
output[ii] = 2*jj+1
return output
@jit(nopython=True)
def __connect_faces_periodic(face_uni_lo,face_uni_hi,points,dist,idx,search_dist):
N = len(face_uni_hi)
output = np.zeros((N,4),dtype=face_uni_lo.dtype)
jj = 0
for ii in range(N):
if dist[ii]<search_dist:
output[jj,:] = (3,face_uni_hi[ii],face_uni_lo[idx[ii]],face_uni_hi[ii])
jj += 1
return output[:jj,:]
#####
tol_overlap = 1e-5*self.spacing
bd_face_tag = __get_boundary_faces(faces,points,tuple(contour.bounds),tol_overlap)
#####
faces_connect = [faces]
for axis in range(3): for axis in range(3):
# Build a nearest-neighbor search KD-trees for boundary points so that we can connect if not self.periodicity[axis]: continue
# them to the boundary faces when needed
# t__ = time()
pos_bd_lo = self.origin[axis]
pos_bd_hi = self.origin[axis]+self.spacing[axis]*(self.dimensions[axis]-1)
search_dist = 1e-5*self.spacing[axis]
idx_lo = np.flatnonzero(np.abs(contour.points[:,axis]-pos_bd_lo)<search_dist)
idx_hi = np.flatnonzero(np.abs(contour.points[:,axis]-pos_bd_lo)<search_dist)
sl_pln = [0,1,2] sl_pln = [0,1,2]
del sl_pln[axis] del sl_pln[axis]
# print(len(idx_lo),len(idx_hi)) face_uni_lo = np.unique(faces[bd_face_tag==2*axis,1:].ravel())
# kd_lo = KDTree(contour.points[np.ix_(idx_lo,sl_pln)],leafsize=10,compact_nodes=True,copy_data=False,balanced_tree=True) face_uni_hi = np.unique(faces[bd_face_tag==2*axis+1,1:].ravel())
# kd_hi = KDTree(contour.points[np.ix_(idx_hi,sl_pln)],leafsize=10,compact_nodes=True,copy_data=False,balanced_tree=True) kdt = KDTree(points[np.ix_(face_uni_lo,sl_pln)],leafsize=10,compact_nodes=False,
# print('KD tree build:',time()-t__) copy_data=False,balanced_tree=False)
# Compute the contour on the boundary: the normal should point outwards. dist,idx = kdt.query(points[np.ix_(face_uni_hi,sl_pln)],k=1,
sl_lo = 3*[slice(None)] distance_upper_bound=tol_overlap[axis])
sl_lo[axis] = 0 faces_connect += [__connect_faces_periodic(face_uni_lo,face_uni_hi,points,
sl_hi = 3*[slice(None)] dist,idx,tol_overlap[axis])]
sl_hi[axis] = -1 #####
origin = self.origin.copy() ncells = faces.shape[0]
origin[axis] -= self.spacing[axis] contour.faces = np.concatenate(faces_connect,axis=0)
dimensions = self.dimensions.copy() #####
dimensions[axis] = 2 alg = vtk.vtkPolyDataConnectivityFilter()
#
tmp = np.empty(dimensions,dtype=self._input.dtype,order='F')
tmp[tuple(sl_hi)] = self._input[tuple(sl_lo)]
tmp[tuple(sl_lo)] = -1e-30
print(origin)
print(self.spacing)
print(dimensions)
#
planevtk = pv.UniformGrid()
planevtk.dimensions = dimensions
planevtk.spacing = self.spacing
planevtk.origin = origin
planevtk.point_arrays['data'] = tmp.ravel('F')
# Contour for lower boundary
contour_bd = planevtk.contour([self._threshold],method=contour_method)
faces_bd = contour_bd.faces.reshape(-1,4).copy()
points_bd = contour_bd.points.copy()
print(points_bd)
# Find points which connect to main contour and update faces
kd = KDTree(points_bd[:,sl_pln],leafsize=10,compact_nodes=True,copy_data=False,balanced_tree=True)
kddist = 1e-1*self.spacing[axis]
print(kddist)
# ptidx = kd.query(contour.points[np.ix_(idx_lo,sl_pln)],k=1)#,distance_upper_bound=kddist)
ptidx = kd.query(contour.points[np.ix_(idx_lo,sl_pln)],k=1)#,distance_upper_bound=kddist)
# print(ptidx[0])
# print(np.min(contour.points[idx_lo,0]),np.max(contour.points[idx_lo,0]))
print(np.min(points_bd[:,0]),np.max(points_bd[:,0]))
print('connections:',np.sum(ptidx[0]<kddist))
print('boundary points:',points_bd[:,sl_pln].shape)
print('selected points:',contour.points[np.ix_(idx_lo,sl_pln)].shape)
stop
#
if self.periodicity[axis]:
sl_swap = [1,2,3]
del sl_swap[axis]
self._faces_bd += [contour_bd.faces.reshape(-1,4).copy()]
self._faces_bd[-1][:,sl_swap] = self._faces_bd[-1][:,sl_swap[::-1]]
self._points_bd += [contour_bd.points.copy()]
self._points_bd[-1][axis] += self.spacing[axis]*(self.dimensions[axis]-1)
else:
origin[axis] = self.origin[axis]+self.spacing[axis]*(self.dimensions[axis]-1)
tmp[tuple(sl_lo)] = self._input[tuple(sl_hi)]
tmp[tuple(sl_hi)] = -1e-30
planevtk.origin = origin
planevtk.point_arrays['data'] = tmp.ravel('F')
contour_bd = planevtk.contour([self._threshold],method=contour_method)
self._faces_bd += [contour_bd.faces.reshape(-1,4).copy()]
self._points_bd += [contour_bd.points.copy()]
print('boundary contour:',time()-t__)
#
# Compute the connectivity of the triangulated surface: first we run an ordinary
# connectivity filter neglecting periodic wrapping.
if report: print('[Features3d.triangulate] computing connectivity...')
alg = vtk.vtkPolyDataConnectivityFilter() # ~twice as fast as default vtkConnectivityFilter
alg.SetInputData(contour) alg.SetInputData(contour)
alg.SetExtractionModeToAllRegions() alg.SetExtractionModeToAllRegions()
alg.SetColorRegions(True) alg.SetColorRegions(True)
alg.Update() alg.Update()
contour = pv.filters._get_output(alg) contour = pv.filters._get_output(alg)
# Now determine the boundary points, i.e. points on the boundary which have a corresponding # RegionIds are now stored as point data. To efficiently convert it to cell data, the first
# vertex on the other side of the domain
if report: print('[Features3d.triangulate] correcting connectivity for periodic boundaries...')
bd_points_xyz = np.zeros((0,3),dtype=contour.points.dtype)
bd_points_idx = np.zeros((0,),dtype=np.int)
for axis in range(3):
if not self.periodicity[axis]: continue
# Compute position of boundary, period of domain and set a threshold for "on boundary" condition
bound_pos = datavtk.origin[axis]+datavtk.spacing[axis]*(datavtk.dimensions[axis]-1)
period = np.zeros((3,))
period[axis] = bound_pos-datavtk.origin[axis]
bound_dist = 1e-5*datavtk.spacing[axis]
# Lower boundary
li = np.abs(contour.points[:,axis]-datavtk.origin[axis])<bound_dist
bd_points_idx = np.append(bd_points_idx,np.nonzero(li)[0],axis=0)
bd_points_xyz = np.append(bd_points_xyz,contour.points[li,:],axis=0)
# Upper boundary
li = np.abs(contour.points[:,axis]-bound_pos)<bound_dist
bd_points_idx = np.append(bd_points_idx,np.nonzero(li)[0],axis=0)
bd_points_xyz = np.append(bd_points_xyz,contour.points[li,:]-period,axis=0)
# Construct a KD Tree for efficient neighborhood search
kd = spatial.KDTree(bd_points_xyz,leafsize=10,compact_nodes=True,copy_data=False,balanced_tree=True)
# Construct a map to get new labels for regions. We search for pairs of points with
# a very small distance inbetween. Then we check if their labels differ and choose
# the lower label as the new joint one.
point_labels = contour.point_arrays['RegionId']
nfeatures = np.max(point_labels)+1
map_ = np.array(range(nfeatures),dtype=point_labels.dtype)
overlap_dist = 1e-4*np.sqrt(np.square(datavtk.spacing).sum())
for (ii,jj) in kd.query_pairs(r=overlap_dist):
label_ii = point_labels[bd_points_idx[ii]]
label_jj = point_labels[bd_points_idx[jj]]
if label_ii!=label_jj:
source_ = np.maximum(label_ii,label_jj)
target_ = np.minimum(label_ii,label_jj)
while target_ != map_[target_]: # map it recursively
target_ = map_[target_]
map_[source_] = target_
map_ = np.unique(map_,return_inverse=True)[1]
point_labels = map_[point_labels]
self._nfeatures = np.max(map_)+1 # starts with zero
# Labels are now stored as point data. To efficiently convert it to cell data, the first
# point of each cell determines the value for this cell. # point of each cell determines the value for this cell.
regionid = contour.point_arrays['RegionId'][contour.faces.reshape(-1,4)[:ncells,1]].ravel()
nregion = np.amax(regionid)+1
# Store the face/vertex data in class arrays. Sort the faces by RegionId for quick access
# later on.
if report: print('[Features3d.triangulate] sorting faces by labels...') if report: print('[Features3d.triangulate] sorting faces by labels...')
cell_labels = point_labels[contour.faces.reshape(contour.n_faces,4)[:,1]] self._points = points
offset = np.zeros((self._nfeatures+1,),dtype=np.int64) self._nfeatures = nregion
offset[1:] = np.cumsum(np.bincount(cell_labels)) #
ind = np.argsort(cell_labels) idx = np.flatnonzero(bd_face_tag<0)
self._offset = offset idxbd = np.flatnonzero(bd_face_tag>=0)
self._faces = contour.faces.reshape(contour.n_faces,4)[ind,:] self._offset = np.zeros((nregion+1,),dtype=np.int64)
self._points = contour.points self._offset[1:] = np.cumsum(np.bincount(regionid[idx],minlength=nregion))
# Compute the volume and area per cell. For the volume computation, an arbitrary component self._offsetbd = np.full((nregion+1,),self._offset[-1],dtype=np.int64)
# of the normal has to be chosen which defaults to the z-component and is set by self._offsetbd[1:] += np.cumsum(np.bincount(regionid[idxbd],minlength=nregion))
# 'cellvol_normal_component'. self._faces = np.concatenate((
if report: print('[Features3d.triangulate] calculating area and volume per cell...') faces[idx,:][np.argsort(regionid[idx]),:],
A = self._points[self._faces[:,1],:] faces[idxbd,:][np.argsort(regionid[idxbd]),:]),axis=0)
B = self._points[self._faces[:,2],:] # #
C = self._points[self._faces[:,3],:] # # self._gradient = (contour.point_arrays['Gradients'][self._faces[:,1],:]
cn = np.cross(B-A,C-A)
# Check if cell normal points in direction of gradient. If not, switch vertex order. # # Compute the volume and area per cell. For the volume computation, an arbitrary component
idx = (contour.point_arrays['Gradients'][self._faces[:,1],:]*cn).sum(axis=-1)>0 # # of the normal has to be chosen which defaults to the z-component and is set by
# print(idx.shape,np.sum(idx),self._faces.shape,self._faces[idx,2:].shape,self._faces[idx,3:1:-1].shape) # # 'cellvol_normal_component'.
# self._faces[np.ix_(idx,[2,3])] = self._faces[np.ix_(idx,[3,2])] # if report: print('[Features3d.triangulate] calculating area and volume per cell...')
# cn[idx,:] = -cn[idx,:] # A = self._points[self._faces[:,1],:]
# Compute area and signed volume per cell # B = self._points[self._faces[:,2],:]
cc = (A+B+C)/3 # C = self._points[self._faces[:,3],:]
self._cell_areas = 0.5*np.sqrt(np.square(cn).sum(axis=1)) # cn = np.cross(B-A,C-A)
self._cell_volumes = 0.5*cn[:,cellvol_normal_component]*cc[:,cellvol_normal_component] # # Check if cell normal points in direction of gradient. If not, switch vertex order.
# idx = (contour.point_arrays['Gradients'][self._faces[:,1],:]*cn).sum(axis=-1)>0
# # print(idx.shape,np.sum(idx),self._faces.shape,self._faces[idx,2:].shape,self._faces[idx,3:1:-1].shape)
# # self._faces[np.ix_(idx,[2,3])] = self._faces[np.ix_(idx,[3,2])]
# # cn[idx,:] = -cn[idx,:]
# # Compute area and signed volume per cell
# cc = (A+B+C)/3
# self._cell_areas = 0.5*np.sqrt(np.square(cn).sum(axis=1))
# self._cell_volumes = 0.5*cn[:,cellvol_normal_component]*cc[:,cellvol_normal_component]
return return
@property @property
@ -967,6 +917,8 @@ class Features3d:
'''Returns an array with volumes of all features.''' '''Returns an array with volumes of all features.'''
return np.add.reduceat(self._cell_volumes,self._offset[:-1]) return np.add.reduceat(self._cell_volumes,self._offset[:-1])
def build_kdtree(self,kdaxis=0,leafsize=100,compact_nodes=False,balanced_tree=False,report=False): def build_kdtree(self,kdaxis=0,leafsize=100,compact_nodes=False,balanced_tree=False,report=False):
'''Builds a KD-tree for feature search.''' '''Builds a KD-tree for feature search.'''
from scipy import spatial from scipy import spatial
@ -1074,28 +1026,42 @@ class Features3d:
# checking any value afterwards. # checking any value afterwards.
return np.searchsorted(self._offset,idx_cell,side='right')-1 return np.searchsorted(self._offset,idx_cell,side='right')-1
def faces_from_feature(self,features,concat=True): def faces_from_feature(self,features,incl_boundary=False):
'''Returns indices of cells which belong to given features.''' '''Returns indices of cells which belong to given features.'''
from collections.abc import Iterable from collections.abc import Iterable
faces = []
if features is None: if features is None:
idx = slice(None) faces.append(self._faces)
if incl_boundary:
faces.append(self._faces)
elif not isinstance(features,Iterable): elif not isinstance(features,Iterable):
idx = slice(self._offset[features],self._offset[features+1]) idx = slice(self._offset[features],self._offset[features+1])
elif concat: faces.append(self._faces[idx,:])
idx = [] if incl_boundary:
idx = slice(self._offsetbd[features],self._offsetbd[features+1])
faces.append(self._faces[idx,:])
else:
for feature in features: for feature in features:
rng_ = np.arange(self._offset[feature],self._offset[feature+1]) idx = np.arange(self._offset[feature],self._offset[feature+1])
idx.append(rng_) faces.append(self._faces[idx,:])
if len(idx)==1: if incl_boundary:
idx = np.array(idx) for feature in features:
else: idx = np.arange(self._offsetbd[feature],self._offsetbd[feature+1])
idx = np.concatenate(idx,axis=0) faces.append(self._faces[idx,:])
else: return np.concatenate(faces,axis=0)
idx = []
def regionid_from_feature(self,features,incl_boundary=False):
'''Returns regionid cell array to be added to PolyData.'''
features = self.list_of_features(features)
regionid = []
for feature in features:
nfaces = self._offset[feature+1]-self._offset[feature]
regionid.append(np.full(nfaces,feature,dtype=np.int64))
if incl_boundary:
for feature in features: for feature in features:
rng_ = slice(self._offset[feature],self._offset[feature+1]) nfaces = self._offsetbd[feature+1]-self._offsetbd[feature]
idx.append(rng_) regionid.append(np.full(nfaces,feature,dtype=np.int64))
return idx return np.concatenate(regionid,axis=0)
def nfaces_of_feature(self,features): def nfaces_of_feature(self,features):
'''Returns number of cells which belong to given features. Can be used '''Returns number of cells which belong to given features. Can be used
@ -1217,10 +1183,15 @@ class Features3d:
icolor = (icolor+1)%ncolor icolor = (icolor+1)%ncolor
return ListedColormap(output) return ListedColormap(output)
def to_vtk(self,features): def to_vtk(self,features,incl_regionid=False,incl_boundary=False):
import pyvista as pv import pyvista as pv
idx = self.faces_from_feature(features) faces = self.faces_from_feature(features,incl_boundary=incl_boundary)
return pv.PolyData(self._points,self._faces[idx,:]) print(faces.shape)
output = pv.PolyData(self._points,faces)
print(output.n_faces)
if incl_regionid:
output.cell_arrays['RegionId'] = self.regionid_from_feature(features,incl_boundary=incl_boundary)
return output
def to_vtk2(self,features): def to_vtk2(self,features):
import pyvista as pv import pyvista as pv