diff --git a/python/damask/_geom.py b/python/damask/_geom.py index 7e6b6a1d3..b7a7928ca 100644 --- a/python/damask/_geom.py +++ b/python/damask/_geom.py @@ -17,29 +17,44 @@ from . import grid_filters class Geom: """Geometry definition for grid solvers.""" - def __init__(self,microstructure,size,origin=[0.0,0.0,0.0],homogenization=1,comments=[]): + def __init__(self,materials,size,origin=[0.0,0.0,0.0],comments=[]): """ - New geometry definition from array of microstructures and size. + New geometry definition from array of materials, size, and origin. Parameters ---------- - microstructure : numpy.ndarray - Microstructure array (3D) + materials : numpy.ndarray + Material index array (3D). size : list or numpy.ndarray - Physical size of the microstructure in meter. + Physical size of the geometry in meter. origin : list or numpy.ndarray, optional - Physical origin of the microstructure in meter. - homogenization : int, optional - Homogenization index. + Physical origin of the geometry in meter. comments : list of str, optional Comment lines. """ - self.set_microstructure(microstructure,inplace=True) - self.set_size(size,inplace=True) - self.set_origin(origin,inplace=True) - self.set_homogenization(homogenization,inplace=True) - self.set_comments(comments,inplace=True) + if len(materials.shape) != 3: + raise ValueError(f'Invalid materials shape {materials.shape}.') + elif materials.dtype not in np.sctypes['float'] + np.sctypes['int']: + raise TypeError(f'Invalid materials data type {materials.dtype}.') + else: + self.materials = np.copy(materials) + + if self.materials.dtype in np.sctypes['float'] and \ + np.all(self.materials == self.materials.astype(int).astype(float)): + self.materials = self.materials.astype(int) + + if len(size) != 3 or any(np.array(size) <= 0): + raise ValueError(f'Invalid size {size}.') + else: + self.size = np.array(size) + + if len(origin) != 3: + raise ValueError(f'Invalid origin {origin}.') + else: + self.origin = np.array(origin) + + self.comments = [str(c) for c in comments] if isinstance(comments,list) else [str(comments)] def __repr__(self): @@ -48,8 +63,8 @@ class Geom: f'grid a b c: {util.srepr(self.grid, " x ")}', f'size x y z: {util.srepr(self.size, " x ")}', f'origin x y z: {util.srepr(self.origin," ")}', - f'# materialpoints: {self.N_microstructure}', - f'max materialpoint: {np.nanmax(self.microstructure)}', + f'# materialpoints: {self.N_materials}', + f'max materialpoint: {np.nanmax(self.materials)}', ]) @@ -63,42 +78,6 @@ class Geom: return self.__copy__() - def duplicate(self,microstructure=None,size=None,origin=None,comments=None,autosize=False): - """ - Create a duplicate having updated microstructure, size, and origin. - - Parameters - ---------- - microstructure : numpy.ndarray, optional - Microstructure array (3D). - size : list or numpy.ndarray, optional - Physical size of the microstructure in meter. - origin : list or numpy.ndarray, optional - Physical origin of the microstructure in meter. - comments : list of str, optional - Comment lines. - autosize : bool, optional - Ignore size parameter and rescale according to change of grid points. - - """ - if size is not None and autosize: - raise ValueError('Auto-sizing conflicts with explicit size parameter.') - - grid_old = self.grid - dup = self.set_microstructure(microstructure)\ - .set_origin(origin) - - if comments is not None: - dup.set_comments(comments,inplace=True) - - if size is not None: - dup.set_size(size,inplace=True) - elif autosize: - dup.set_size(dup.grid/grid_old*self.size,inplace=True) - - return dup - - def diff(self,other): """ Report property differences of self relative to other. @@ -114,154 +93,33 @@ class Geom: message.append(util.delete(f'grid a b c: {util.srepr(other.grid," x ")}')) message.append(util.emph( f'grid a b c: {util.srepr( self.grid," x ")}')) - if np.any(other.size != self.size): + if not np.allclose(other.size,self.size): message.append(util.delete(f'size x y z: {util.srepr(other.size," x ")}')) message.append(util.emph( f'size x y z: {util.srepr( self.size," x ")}')) - if np.any(other.origin != self.origin): + if not np.allclose(other.origin,self.origin): message.append(util.delete(f'origin x y z: {util.srepr(other.origin," ")}')) message.append(util.emph( f'origin x y z: {util.srepr( self.origin," ")}')) - if other.N_microstructure != self.N_microstructure: - message.append(util.delete(f'# materialpoints: {other.N_microstructure}')) - message.append(util.emph( f'# materialpoints: { self.N_microstructure}')) + if other.N_materials != self.N_materials: + message.append(util.delete(f'# materialpoints: {other.N_materials}')) + message.append(util.emph( f'# materialpoints: { self.N_materials}')) - if np.nanmax(other.microstructure) != np.nanmax(self.microstructure): - message.append(util.delete(f'max materialpoint: {np.nanmax(other.microstructure)}')) - message.append(util.emph( f'max materialpoint: {np.nanmax( self.microstructure)}')) + if np.nanmax(other.materials) != np.nanmax(self.materials): + message.append(util.delete(f'max materialpoint: {np.nanmax(other.materials)}')) + message.append(util.emph( f'max materialpoint: {np.nanmax( self.materials)}')) return util.return_message(message) - def set_comments(self,comments,inplace=False): - """ - Replace all existing comments. - - Parameters - ---------- - comments : list of str - All comments. - - """ - target = self if inplace else self.copy() - target.comments = [] - target.add_comments(comments,inplace=True) - if not inplace: return target - - - def add_comments(self,comments,inplace=False): - """ - Append comments to existing comments. - - Parameters - ---------- - comments : list of str - New comments. - - """ - target = self if inplace else self.copy() - target.comments += [str(c) for c in comments] if isinstance(comments,list) else [str(comments)] - if not inplace: return target - - - def set_microstructure(self,microstructure,inplace=False): - """ - Replace the existing microstructure representation. - - The complete microstructure is replaced (indcluding grid definition), - unless a masked array is provided in which case the grid dimensions - need to match and masked entries are not replaced. - - Parameters - ---------- - microstructure : numpy.ndarray or numpy.ma.core.MaskedArray of shape (:,:,:) - Microstructure indices. - - """ - target = self if inplace else self.copy() - if microstructure is not None: - if isinstance(microstructure,np.ma.core.MaskedArray): - target.microstructure = np.where(microstructure.mask, - target.microstructure,microstructure.data) - else: - target.microstructure = np.copy(microstructure) - - if target.microstructure.dtype in np.sctypes['float'] and \ - np.all(target.microstructure == target.microstructure.astype(int).astype(float)): - target.microstructure = target.microstructure.astype(int) - - if len(target.microstructure.shape) != 3: - raise ValueError(f'Invalid microstructure shape {microstructure.shape}') - elif target.microstructure.dtype not in np.sctypes['float'] + np.sctypes['int']: - raise TypeError(f'Invalid microstructure data type {microstructure.dtype}') - if not inplace: return target - - - def set_size(self,size,inplace=False): - """ - Replace the existing size information. - - Parameters - ---------- - size : list or numpy.ndarray - Physical size of the microstructure in meter. - - """ - target = self if inplace else self.copy() - if size is not None: - if len(size) != 3 or any(np.array(size) <= 0): - raise ValueError(f'Invalid size {size}') - else: - target.size = np.array(size) - if not inplace: return target - - - def set_origin(self,origin,inplace=False): - """ - Replace the existing origin information. - - Parameters - ---------- - origin : list or numpy.ndarray - Physical origin of the microstructure in meter. - - """ - target = self if inplace else self.copy() - if origin is not None: - if len(origin) != 3: - raise ValueError(f'Invalid origin {origin}') - else: - target.origin = np.array(origin) - if not inplace: return target - - - def set_homogenization(self,homogenization,inplace=False): - """ - Replace the existing homogenization index. - - Parameters - ---------- - homogenization : int - Homogenization index. - - """ - target = self if inplace else self.copy() - if homogenization is not None: - if not isinstance(homogenization,int) or homogenization < 1: - raise TypeError(f'Invalid homogenization {homogenization}.') - else: - target.homogenization = homogenization - if not inplace: return target - - @property def grid(self): - return np.asarray(self.microstructure.shape) + return np.asarray(self.materials.shape) @property - def N_microstructure(self): - return np.unique(self.microstructure).size + def N_materials(self): + return np.unique(self.materials).size @staticmethod @@ -301,12 +159,10 @@ class Geom: size = np.array([float(dict(zip(items[1::2],items[2::2]))[i]) for i in ['x','y','z']]) elif key == 'origin': origin = np.array([float(dict(zip(items[1::2],items[2::2]))[i]) for i in ['x','y','z']]) - elif key == 'homogenization': - homogenization = int(items[1]) else: comments.append(line.strip()) - microstructure = np.empty(grid.prod()) # initialize as flat array + materials = np.empty(grid.prod()) # initialize as flat array i = 0 for line in content[header_length:]: items = line.split('#')[0].split() @@ -318,16 +174,16 @@ class Geom: abs(int(items[2])-int(items[0]))+1,dtype=float) else: items = list(map(float,items)) else: items = list(map(float,items)) - microstructure[i:i+len(items)] = items + materials[i:i+len(items)] = items i += len(items) if i != grid.prod(): raise TypeError(f'Invalid file: expected {grid.prod()} entries, found {i}') - if not np.any(np.mod(microstructure,1) != 0.0): # no float present - microstructure = microstructure.astype('int') + if not np.any(np.mod(materials,1) != 0.0): # no float present + materials = materials.astype('int') - return Geom(microstructure.reshape(grid,order='F'),size,origin,homogenization,comments) + return Geom(materials.reshape(grid,order='F'),size,origin,comments) @staticmethod @@ -365,7 +221,7 @@ class Geom: grid : int numpy.ndarray of shape (3) Number of grid points in x,y,z direction. size : list or numpy.ndarray of shape (3) - Physical size of the microstructure in meter. + Physical size of the geometry in meter. seeds : numpy.ndarray of shape (:,3) Position of the seed points in meter. All points need to lay within the box. weights : numpy.ndarray of shape (seeds.shape[0]) @@ -389,16 +245,17 @@ class Geom: result = pool.map_async(partial(Geom._find_closest_seed,seeds_p,weights_p), [coord for coord in coords]) pool.close() pool.join() - microstructure = np.array(result.get()) + materials = np.array(result.get()) if periodic: - microstructure = microstructure.reshape(grid*3) - microstructure = microstructure[grid[0]:grid[0]*2,grid[1]:grid[1]*2,grid[2]:grid[2]*2]%seeds.shape[0] + materials = materials.reshape(grid*3) + materials = materials[grid[0]:grid[0]*2,grid[1]:grid[1]*2,grid[2]:grid[2]*2]%seeds.shape[0] else: - microstructure = microstructure.reshape(grid) + materials = materials.reshape(grid) - return Geom(microstructure+1,size,homogenization=1, - comments=util.execution_stamp('Geom','from_Laguerre_tessellation'), + return Geom(materials = materials+1, + size = size, + comments = util.execution_stamp('Geom','from_Laguerre_tessellation'), ) @@ -412,7 +269,7 @@ class Geom: grid : int numpy.ndarray of shape (3) Number of grid points in x,y,z direction. size : list or numpy.ndarray of shape (3) - Physical size of the microstructure in meter. + Physical size of the geometry in meter. seeds : numpy.ndarray of shape (:,3) Position of the seed points in meter. All points need to lay within the box. periodic : Boolean, optional @@ -421,10 +278,11 @@ class Geom: """ coords = grid_filters.cell_coord0(grid,size).reshape(-1,3) KDTree = spatial.cKDTree(seeds,boxsize=size) if periodic else spatial.cKDTree(seeds) - devNull,microstructure = KDTree.query(coords) + devNull,materials = KDTree.query(coords) - return Geom(microstructure.reshape(grid)+1,size,homogenization=1, - comments=util.execution_stamp('Geom','from_Voronoi_tessellation'), + return Geom(materials = materials.reshape(grid)+1, + size = size, + comments = util.execution_stamp('Geom','from_Voronoi_tessellation'), ) @@ -462,21 +320,21 @@ class Geom: +[ 'grid a {} b {} c {}'.format(*geom.grid), 'size x {} y {} z {}'.format(*geom.size), 'origin x {} y {} z {}'.format(*geom.origin), - f'homogenization {geom.homogenization}', + 'homogenization 1', ] grid = geom.grid if pack is None: - plain = grid.prod()/geom.N_microstructure < 250 + plain = grid.prod()/geom.N_materials < 250 else: plain = not pack if plain: - format_string = '%g' if geom.microstructure.dtype in np.sctypes['float'] else \ - '%{}i'.format(1+int(np.floor(np.log10(np.nanmax(geom.microstructure))))) + format_string = '%g' if geom.materials.dtype in np.sctypes['float'] else \ + '%{}i'.format(1+int(np.floor(np.log10(np.nanmax(geom.materials))))) np.savetxt(fname, - geom.microstructure.reshape([grid[0],np.prod(grid[1:])],order='F').T, + geom.materials.reshape([grid[0],np.prod(grid[1:])],order='F').T, header='\n'.join(header), fmt=format_string, comments='') else: try: @@ -487,7 +345,7 @@ class Geom: compressType = None former = start = -1 reps = 0 - for current in geom.microstructure.flatten('F'): + for current in geom.materials.flatten('F'): if abs(current - former) == 1 and (start - current) == reps*(former - current): compressType = 'to' reps += 1 @@ -532,7 +390,7 @@ class Geom: """ v = VTK.from_rectilinearGrid(geom.grid,geom.size,geom.origin) - v.add(geom.microstructure.flatten(order='F'),'materialpoint') + v.add(geom.materials.flatten(order='F'),'materialpoint') v.add_comments(geom.comments) if fname: @@ -575,11 +433,11 @@ class Geom: 0 gives octahedron (|x|^(2^0) + |y|^(2^0) + |z|^(2^0) < 1) 1 gives a sphere (|x|^(2^1) + |y|^(2^1) + |z|^(2^1) < 1) fill : int, optional - Fill value for primitive. Defaults to microstructure.max() + 1. + Fill value for primitive. Defaults to materials.max() + 1. R : damask.Rotation, optional Rotation of primitive. Defaults to no rotation. inverse : Boolean, optional - Retain original microstructure within primitive and fill outside. + Retain original materials within primitive and fill outside. Defaults to False. periodic : Boolean, optional Repeat primitive over boundaries. Defaults to True. @@ -601,22 +459,23 @@ class Geom: if periodic: # translate back to center mask = np.roll(mask,((c-np.ones(3)*.5)*self.grid).astype(int),(0,1,2)) - fill_ = np.full_like(self.microstructure,np.nanmax(self.microstructure)+1 if fill is None else fill) - ms = np.ma.MaskedArray(fill_,np.logical_not(mask) if inverse else mask) + fill_ = np.full_like(self.materials,np.nanmax(self.materials)+1 if fill is None else fill) - return self.duplicate(ms, - comments=self.comments+[util.execution_stamp('Geom','add_primitive')], - ) + return Geom(materials = np.where(np.logical_not(mask) if inverse else mask, self.materials,fill_), + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','add_primitive')], + ) def mirror(self,directions,reflect=False): """ - Mirror microstructure along given directions. + Mirror geometry along given directions. Parameters ---------- directions : iterable containing str - Direction(s) along which the microstructure is mirrored. + Direction(s) along which the geometry is mirrored. Valid entries are 'x', 'y', 'z'. reflect : bool, optional Reflect (include) outermost layers. Defaults to False. @@ -627,28 +486,30 @@ class Geom: raise ValueError(f'Invalid direction {set(directions).difference(valid)} specified.') limits = [None,None] if reflect else [-2,0] - ms = self.microstructure.copy() + ms = self.materials.copy() - if 'z' in directions: - ms = np.concatenate([ms,ms[:,:,limits[0]:limits[1]:-1]],2) - if 'y' in directions: - ms = np.concatenate([ms,ms[:,limits[0]:limits[1]:-1,:]],1) if 'x' in directions: ms = np.concatenate([ms,ms[limits[0]:limits[1]:-1,:,:]],0) + if 'y' in directions: + ms = np.concatenate([ms,ms[:,limits[0]:limits[1]:-1,:]],1) + if 'z' in directions: + ms = np.concatenate([ms,ms[:,:,limits[0]:limits[1]:-1]],2) - return self.duplicate(ms, - comments=self.comments+[util.execution_stamp('Geom','mirror')], - autosize=True) + return Geom(materials = ms, + size = self.size/self.grid*np.asarray(ms.shape), + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','mirror')], + ) def flip(self,directions): """ - Flip microstructure along given directions. + Flip geometry along given directions. Parameters ---------- directions : iterable containing str - Direction(s) along which the microstructure is flipped. + Direction(s) along which the geometry is flipped. Valid entries are 'x', 'y', 'z'. """ @@ -656,16 +517,18 @@ class Geom: if not set(directions).issubset(valid): raise ValueError(f'Invalid direction {set(directions).difference(valid)} specified.') - ms = np.flip(self.microstructure, (valid.index(d) for d in directions if d in valid)) + ms = np.flip(self.materials, (valid.index(d) for d in directions if d in valid)) - return self.duplicate(ms, - comments=self.comments+[util.execution_stamp('Geom','flip')], - ) + return Geom(materials = ms, + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','flip')], + ) def scale(self,grid,periodic=True): """ - Scale microstructure to new grid. + Scale geometry to new grid. Parameters ---------- @@ -675,21 +538,23 @@ class Geom: Assume geometry to be periodic. Defaults to True. """ - return self.duplicate(ndimage.interpolation.zoom( - self.microstructure, - grid/self.grid, - output=self.microstructure.dtype, - order=0, - mode=('wrap' if periodic else 'nearest'), - prefilter=False - ), - comments=self.comments+[util.execution_stamp('Geom','scale')], - ) + return Geom(materials = ndimage.interpolation.zoom( + self.materials, + grid/self.grid, + output=self.materials.dtype, + order=0, + mode=('wrap' if periodic else 'nearest'), + prefilter=False + ), + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','scale')], + ) def clean(self,stencil=3,selection=None,periodic=True): """ - Smooth microstructure by selecting most frequent index within given stencil at each location. + Smooth geometry by selecting most frequent material index within given stencil at each location. Parameters ---------- @@ -709,83 +574,87 @@ class Geom: else: return me - return self.duplicate(ndimage.filters.generic_filter( - self.microstructure, - mostFrequent, - size=(stencil if selection is None else stencil//2*2+1,)*3, - mode=('wrap' if periodic else 'nearest'), - extra_keywords=dict(selection=selection), - ).astype(self.microstructure.dtype), - comments=self.comments+[util.execution_stamp('Geom','clean')], - ) + return Geom(materials = ndimage.filters.generic_filter( + self.materials, + mostFrequent, + size=(stencil if selection is None else stencil//2*2+1,)*3, + mode=('wrap' if periodic else 'nearest'), + extra_keywords=dict(selection=selection), + ).astype(self.materials.dtype), + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','clean')], + ) def renumber(self): - """Renumber sorted microstructure indices to 1,...,N.""" - renumbered = np.empty(self.grid,dtype=self.microstructure.dtype) - for i, oldID in enumerate(np.unique(self.microstructure)): - renumbered = np.where(self.microstructure == oldID, i+1, renumbered) + """Renumber sorted material indices to 1,...,N.""" + renumbered = np.empty(self.grid,dtype=self.materials.dtype) + for i, oldID in enumerate(np.unique(self.materials)): + renumbered = np.where(self.materials == oldID, i+1, renumbered) - return self.duplicate(renumbered, - comments=self.comments+[util.execution_stamp('Geom','renumber')], - ) + return Geom(materials = renumbered, + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','renumber')], + ) def rotate(self,R,fill=None): """ - Rotate microstructure (pad if required). + Rotate geometry (pad if required). Parameters ---------- R : damask.Rotation - Rotation to apply to the microstructure. + Rotation to apply to the geometry. fill : int or float, optional - Microstructure index to fill the corners. Defaults to microstructure.max() + 1. + Material index to fill the corners. Defaults to materials.max() + 1. """ - if fill is None: fill = np.nanmax(self.microstructure) + 1 - dtype = float if np.isnan(fill) or int(fill) != fill or self.microstructure.dtype==np.float else int + if fill is None: fill = np.nanmax(self.materials) + 1 + dtype = float if np.isnan(fill) or int(fill) != fill or self.materials.dtype==np.float else int Eulers = R.as_Eulers(degrees=True) - microstructure_in = self.microstructure.copy() + materials_in = self.materials.copy() # These rotations are always applied in the reference coordinate system, i.e. (z,x,z) not (z,x',z'') # see https://www.cs.utexas.edu/~theshark/courses/cs354/lectures/cs354-14.pdf for angle,axes in zip(Eulers[::-1], [(0,1),(1,2),(0,1)]): - microstructure_out = ndimage.rotate(microstructure_in,angle,axes,order=0, + materials_out = ndimage.rotate(materials_in,angle,axes,order=0, prefilter=False,output=dtype,cval=fill) - if np.prod(microstructure_in.shape) == np.prod(microstructure_out.shape): + if np.prod(materials_in.shape) == np.prod(materials_out.shape): # avoid scipy interpolation errors for rotations close to multiples of 90° - microstructure_in = np.rot90(microstructure_in,k=np.rint(angle/90.).astype(int),axes=axes) + materials_in = np.rot90(materials_in,k=np.rint(angle/90.).astype(int),axes=axes) else: - microstructure_in = microstructure_out + materials_in = materials_out - origin = self.origin-(np.asarray(microstructure_in.shape)-self.grid)*.5 * self.size/self.grid + origin = self.origin-(np.asarray(materials_in.shape)-self.grid)*.5 * self.size/self.grid - return self.duplicate(microstructure_in, - origin=origin, - comments=self.comments+[util.execution_stamp('Geom','rotate')], - autosize=True, - ) + return Geom(materials = materials_in, + size = self.size/self.grid*np.asarray(materials_in.shape), + origin = origin, + comments = self.comments+[util.execution_stamp('Geom','rotate')], + ) def canvas(self,grid=None,offset=None,fill=None): """ - Crop or enlarge/pad microstructure. + Crop or enlarge/pad geometry. Parameters ---------- grid : numpy.ndarray of shape (3) Number of grid points in x,y,z direction. offset : numpy.ndarray of shape (3) - Offset (measured in grid points) from old to new microstructure[0,0,0]. + Offset (measured in grid points) from old to new geometry [0,0,0]. fill : int or float, optional - Microstructure index to fill the background. Defaults to microstructure.max() + 1. + Material index to fill the background. Defaults to materials.max() + 1. """ if offset is None: offset = 0 - if fill is None: fill = np.nanmax(self.microstructure) + 1 - dtype = float if int(fill) != fill or self.microstructure.dtype in np.sctypes['float'] else int + if fill is None: fill = np.nanmax(self.materials) + 1 + dtype = float if int(fill) != fill or self.materials.dtype in np.sctypes['float'] else int canvas = np.full(self.grid if grid is None else grid,fill,dtype) @@ -794,39 +663,41 @@ class Geom: ll = np.clip(-offset, 0,np.minimum( grid,self.grid-offset)) ur = np.clip(-offset+self.grid,0,np.minimum( grid,self.grid-offset)) - canvas[ll[0]:ur[0],ll[1]:ur[1],ll[2]:ur[2]] = self.microstructure[LL[0]:UR[0],LL[1]:UR[1],LL[2]:UR[2]] + canvas[ll[0]:ur[0],ll[1]:ur[1],ll[2]:ur[2]] = self.materials[LL[0]:UR[0],LL[1]:UR[1],LL[2]:UR[2]] - return self.duplicate(canvas, - origin=self.origin+offset*self.size/self.grid, - comments=self.comments+[util.execution_stamp('Geom','canvas')], - autosize=True, - ) + return Geom(materials = canvas, + size = self.size/self.grid*np.asarray(canvas.shape), + origin = self.origin+offset*self.size/self.grid, + comments = self.comments+[util.execution_stamp('Geom','canvas')], + ) - def substitute(self,from_microstructure,to_microstructure): + def substitute(self,from_materials,to_materials): """ - Substitute microstructure indices. + Substitute material indices. Parameters ---------- - from_microstructure : iterable of ints - Microstructure indices to be substituted. - to_microstructure : iterable of ints - New microstructure indices. + from_materials : iterable of ints + Material indices to be substituted. + to_materials : iterable of ints + New material indices. """ - substituted = self.microstructure.copy() - for from_ms,to_ms in zip(from_microstructure,to_microstructure): - substituted[self.microstructure==from_ms] = to_ms + substituted = self.materials.copy() + for from_ms,to_ms in zip(from_materials,to_materials): + substituted[self.materials==from_ms] = to_ms - return self.duplicate(substituted, - comments=self.comments+[util.execution_stamp('Geom','substitute')], - ) + return Geom(materials = substituted, + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','substitute')], + ) def vicinity_offset(self,vicinity=1,offset=None,trigger=[],periodic=True): """ - Offset microstructure index of points in the vicinity of xxx. + Offset material index of points in the vicinity of xxx. Different from themselves (or listed as triggers) within a given (cubic) vicinity, i.e. within the region close to a grain/phase boundary. @@ -835,14 +706,14 @@ class Geom: Parameters ---------- vicinity : int, optional - Voxel distance checked for presence of other microstructure. + Voxel distance checked for presence of other materials. Defaults to 1. offset : int, optional - Offset (positive or negative) to tag microstructure indices, - defaults to microstructure.max() + 1. + Offset (positive or negative) to tag material indices, + defaults to materials.max() + 1. trigger : list of ints, optional - List of microstructure indices triggering a change. - Defaults to [], meaning that different neigboors trigger a change. + List of material indices that trigger a change. + Defaults to [], meaning that any different neighbor triggers a change. periodic : Boolean, optional Assume geometry to be periodic. Defaults to True. @@ -858,14 +729,15 @@ class Geom: trigger = list(trigger) return np.any(np.in1d(stencil,np.array(trigger))) - offset_ = np.nanmax(self.microstructure) if offset is None else offset - mask = ndimage.filters.generic_filter(self.microstructure, + offset_ = np.nanmax(self.materials) if offset is None else offset + mask = ndimage.filters.generic_filter(self.materials, tainted_neighborhood, size=1+2*vicinity, mode='wrap' if periodic else 'nearest', extra_keywords={'trigger':trigger}) - microstructure = np.ma.MaskedArray(self.microstructure + offset_, np.logical_not(mask)) - return self.duplicate(microstructure, - comments=self.comments+[util.execution_stamp('Geom','vicinity_offset')], - ) + return Geom(materials = np.where(mask, self.materials + offset_,self.materials), + size = self.size, + origin = self.origin, + comments = self.comments+[util.execution_stamp('Geom','vicinity_offset')], + ) diff --git a/python/tests/test_Geom.py b/python/tests/test_Geom.py index f82272362..809aee2f9 100644 --- a/python/tests/test_Geom.py +++ b/python/tests/test_Geom.py @@ -11,8 +11,8 @@ from damask import util def geom_equal(a,b): - return np.all(a.microstructure == b.microstructure) and \ - np.all(a.grid == b.grid) and \ + return np.all(a.materials == b.materials) and \ + np.all(a.grid == b.grid) and \ np.allclose(a.size, b.size) and \ str(a.diff(b)) == str(b.diff(a)) @@ -33,64 +33,21 @@ def reference_dir(reference_dir_base): class TestGeom: - @pytest.mark.parametrize('flavor',['plain','explicit']) - def test_duplicate(self,default,flavor): - if flavor == 'plain': - modified = default.duplicate() - elif flavor == 'explicit': - modified = default.duplicate( - default.microstructure, - default.size, - default.origin - ) - print(modified) - assert geom_equal(default,modified) - def test_diff_equal(self,default): assert str(default.diff(default)) == '' + def test_diff_not_equal(self,default): - new = Geom(default.microstructure[1:,1:,1:]+1,default.size*.9,np.ones(3)-default.origin,comments=['modified']) + new = Geom(default.materials[1:,1:,1:]+1,default.size*.9,np.ones(3)-default.origin,comments=['modified']) assert str(default.diff(new)) != '' - def test_set_inplace_outofplace_homogenization(self,default): - default.set_homogenization(123,inplace=True) - outofplace = default.set_homogenization(321,inplace=False) - assert default.homogenization == 123 and outofplace.homogenization == 321 - - - def test_set_inplace_outofplace_microstructure(self,default): - default.set_microstructure(np.arange(72).reshape((2,4,9)),inplace=True) - outofplace = default.set_microstructure(np.arange(72).reshape((8,3,3)),inplace=False) - assert np.array_equal(default.grid,[2,4,9]) and np.array_equal(outofplace.grid,[8,3,3]) - - - def test_set_inplace_outofplace_size(self,default): - default.set_size(np.array([1,2,3]),inplace=True) - outofplace = default.set_size(np.array([3,2,1]),inplace=False) - assert np.array_equal(default.size,[1,2,3]) and np.array_equal(outofplace.size,[3,2,1]) - - - def test_set_inplace_outofplace_comments(self,default): - default.set_comments(['a','and','b'],inplace=True) - outofplace = default.set_comments(['b','or','a'],inplace=False) - assert default.comments == ['a','and','b'] and outofplace.comments == ['b','or','a'] - - - @pytest.mark.parametrize('masked',[True,False]) - def test_set_microstructure(self,default,masked): - old = default.microstructure - new = np.random.randint(200,size=default.grid) - default.set_microstructure(np.ma.MaskedArray(new,np.full_like(new,masked)),inplace=True) - assert np.all(default.microstructure==(old if masked else new)) - - def test_write_read_str(self,default,tmpdir): default.to_file(str(tmpdir/'default.geom'),format='ASCII') new = Geom.from_file(str(tmpdir/'default.geom')) assert geom_equal(default,new) + def test_write_read_file(self,default,tmpdir): with open(tmpdir/'default.geom','w') as f: default.to_file(f,format='ASCII',pack=True) @@ -98,6 +55,7 @@ class TestGeom: new = Geom.from_file(f) assert geom_equal(default,new) + def test_write_as_ASCII(self,default,tmpdir): with open(tmpdir/'str.geom','w') as f: f.write(default.as_ASCII()) @@ -105,6 +63,7 @@ class TestGeom: new = Geom.from_file(f) assert geom_equal(default,new) + def test_read_write_vtr(self,default,tmpdir): default.to_file(tmpdir/'default',format='vtr') for _ in range(10): @@ -114,6 +73,7 @@ class TestGeom: new = Geom.from_vtr(tmpdir/'default.vtr') assert geom_equal(new,default) + def test_invalid_geom(self,tmpdir): with open('invalid_file','w') as f: f.write('this is not a valid header') @@ -121,6 +81,7 @@ class TestGeom: with pytest.raises(TypeError): Geom.from_file(f) + def test_invalid_vtr(self,tmpdir): v = VTK.from_rectilinearGrid(np.random.randint(5,10,3)*2,np.random.random(3) + 1.0) v.to_file(tmpdir/'no_materialpoint.vtr') @@ -137,36 +98,38 @@ class TestGeom: new = Geom.from_file(tmpdir/'default.geom') assert geom_equal(new,default) - def test_invalid_combination(self,default): - with pytest.raises(ValueError): - default.duplicate(default.microstructure[1:,1:,1:],size=np.ones(3), autosize=True) def test_invalid_size(self,default): with pytest.raises(ValueError): - default.duplicate(default.microstructure[1:,1:,1:],size=np.ones(2)) + Geom(default.materials[1:,1:,1:], + size=np.ones(2)) + def test_invalid_origin(self,default): with pytest.raises(ValueError): - default.duplicate(default.microstructure[1:,1:,1:],origin=np.ones(4)) + Geom(default.materials[1:,1:,1:], + size=np.ones(3), + origin=np.ones(4)) - def test_invalid_microstructure_size(self,default): - microstructure = np.ones((3,3)) + + def test_invalid_materials_shape(self,default): + materials = np.ones((3,3)) with pytest.raises(ValueError): - default.duplicate(microstructure) + Geom(materials, + size=np.ones(3)) - def test_invalid_microstructure_type(self,default): - microstructure = np.random.randint(1,300,(3,4,5))==1 - with pytest.raises(TypeError): - default.duplicate(microstructure) - def test_invalid_homogenization(self,default): + def test_invalid_materials_type(self,default): + materials = np.random.randint(1,300,(3,4,5))==1 with pytest.raises(TypeError): - default.set_homogenization(homogenization=0) + Geom(materials) + def test_invalid_write_format(self,default): with pytest.raises(TypeError): default.to_file(format='invalid') + @pytest.mark.parametrize('directions,reflect',[ (['x'], False), (['x','y','z'],True), @@ -182,6 +145,7 @@ class TestGeom: assert geom_equal(Geom.from_file(reference), modified) + @pytest.mark.parametrize('directions',[(1,2,'y'),('a','b','x'),[1]]) def test_mirror_invalid(self,default,directions): with pytest.raises(ValueError): @@ -203,13 +167,16 @@ class TestGeom: assert geom_equal(Geom.from_file(reference), modified) + def test_flip_invariant(self,default): assert geom_equal(default,default.flip([])) + @pytest.mark.parametrize('direction',[['x'],['x','y']]) def test_flip_double(self,default,direction): assert geom_equal(default,default.flip(direction).flip(direction)) + @pytest.mark.parametrize('directions',[(1,2,'y'),('a','b','x'),[1]]) def test_flip_invalid(self,default,directions): with pytest.raises(ValueError): @@ -231,6 +198,7 @@ class TestGeom: current ) + @pytest.mark.parametrize('grid',[ (10,11,10), [10,13,10], @@ -248,22 +216,29 @@ class TestGeom: assert geom_equal(Geom.from_file(reference), modified) + def test_renumber(self,default): - microstructure = default.microstructure.copy() - for m in np.unique(microstructure): - microstructure[microstructure==m] = microstructure.max() + np.random.randint(1,30) - modified = default.duplicate(microstructure) + materials = default.materials.copy() + for m in np.unique(materials): + materials[materials==m] = materials.max() + np.random.randint(1,30) + modified = Geom(materials, + default.size, + default.origin) assert not geom_equal(modified,default) assert geom_equal(default, modified.renumber()) + def test_substitute(self,default): offset = np.random.randint(1,500) - modified = default.duplicate(default.microstructure + offset) + modified = Geom(default.materials + offset, + default.size, + default.origin) assert not geom_equal(modified,default) assert geom_equal(default, - modified.substitute(np.arange(default.microstructure.max())+1+offset, - np.arange(default.microstructure.max())+1)) + modified.substitute(np.arange(default.materials.max())+1+offset, + np.arange(default.materials.max())+1)) + @pytest.mark.parametrize('axis_angle',[np.array([1,0,0,86.7]), np.array([0,1,0,90.4]), np.array([0,0,1,90]), np.array([1,0,0,175]),np.array([0,-1,0,178]),np.array([0,0,1,180])]) @@ -273,6 +248,7 @@ class TestGeom: modified.rotate(Rotation.from_axis_angle(axis_angle,degrees=True)) assert geom_equal(default,modified) + @pytest.mark.parametrize('Eulers',[[32.0,68.0,21.0], [0.0,32.0,240.0]]) def test_rotate(self,default,update,reference_dir,Eulers): @@ -283,11 +259,13 @@ class TestGeom: assert geom_equal(Geom.from_file(reference), modified) + def test_canvas(self,default): grid = default.grid grid_add = np.random.randint(0,30,(3)) modified = default.canvas(grid + grid_add) - assert np.all(modified.microstructure[:grid[0],:grid[1],:grid[2]] == default.microstructure) + assert np.all(modified.materials[:grid[0],:grid[1],:grid[2]] == default.materials) + @pytest.mark.parametrize('center1,center2',[(np.random.random(3)*.5,np.random.random()*8), (np.random.randint(4,8,(3)),np.random.randint(9,12,(3)))]) @@ -300,13 +278,14 @@ class TestGeom: np.random.rand()*4, np.random.randint(20)]) def test_add_primitive_shift(self,center1,center2,diameter,exponent): - """Same volume fraction for periodic microstructures and different center.""" + """Same volume fraction for periodic geometries and different center.""" o = np.random.random(3)-.5 g = np.random.randint(8,32,(3)) s = np.random.random(3)+.5 G_1 = Geom(np.ones(g,'i'),s,o).add_primitive(diameter,center1,exponent) G_2 = Geom(np.ones(g,'i'),s,o).add_primitive(diameter,center2,exponent) - assert np.count_nonzero(G_1.microstructure!=2) == np.count_nonzero(G_2.microstructure!=2) + assert np.count_nonzero(G_1.materials!=2) == np.count_nonzero(G_2.materials!=2) + @pytest.mark.parametrize('center',[np.random.randint(4,10,(3)), np.random.randint(2,10), @@ -323,6 +302,7 @@ class TestGeom: G_2 = Geom(np.ones(g,'i'),[1.,1.,1.]).add_primitive(.3,center,1,fill,Rotation.from_Eulers(eu),inverse,periodic=periodic) assert geom_equal(G_1,G_2) + @pytest.mark.parametrize('trigger',[[1],[]]) def test_vicinity_offset(self,trigger): offset = np.random.randint(2,4) @@ -341,13 +321,15 @@ class TestGeom: geom = Geom(m,np.random.rand(3)).vicinity_offset(vicinity,offset,trigger=trigger) - assert np.all(m2==geom.microstructure) + assert np.all(m2==geom.materials) + @pytest.mark.parametrize('periodic',[True,False]) def test_vicinity_offset_invariant(self,default,periodic): - offset = default.vicinity_offset(trigger=[default.microstructure.max()+1, - default.microstructure.min()-1]) - assert np.all(offset.microstructure==default.microstructure) + offset = default.vicinity_offset(trigger=[default.materials.max()+1, + default.materials.min()-1]) + assert np.all(offset.materials==default.materials) + @pytest.mark.parametrize('periodic',[True,False]) def test_tessellation_approaches(self,periodic): @@ -359,6 +341,7 @@ class TestGeom: Laguerre = Geom.from_Laguerre_tessellation(grid,size,seeds,np.ones(N_seeds),periodic) assert geom_equal(Laguerre,Voronoi) + def test_Laguerre_weights(self): grid = np.random.randint(10,20,3) size = np.random.random(3) + 1.0 @@ -368,17 +351,18 @@ class TestGeom: ms = np.random.randint(1, N_seeds+1) weights[ms-1] = np.random.random() Laguerre = Geom.from_Laguerre_tessellation(grid,size,seeds,weights,np.random.random()>0.5) - assert np.all(Laguerre.microstructure == ms) + assert np.all(Laguerre.materials == ms) + @pytest.mark.parametrize('approach',['Laguerre','Voronoi']) def test_tessellate_bicrystal(self,approach): grid = np.random.randint(5,10,3)*2 size = grid.astype(np.float) seeds = np.vstack((size*np.array([0.5,0.25,0.5]),size*np.array([0.5,0.75,0.5]))) - microstructure = np.ones(grid) - microstructure[:,grid[1]//2:,:] = 2 + materials = np.ones(grid) + materials[:,grid[1]//2:,:] = 2 if approach == 'Laguerre': geom = Geom.from_Laguerre_tessellation(grid,size,seeds,np.ones(2),np.random.random()>0.5) elif approach == 'Voronoi': geom = Geom.from_Voronoi_tessellation(grid,size,seeds, np.random.random()>0.5) - assert np.all(geom.microstructure == microstructure) + assert np.all(geom.materials == materials)