diff --git a/python/damask/_grid.py b/python/damask/_grid.py index 094e8a529..5ef592f45 100644 --- a/python/damask/_grid.py +++ b/python/damask/_grid.py @@ -4,7 +4,7 @@ import warnings import multiprocessing as mp from functools import partial import typing -from typing import Union, Optional, TextIO, List, Sequence +from typing import Union, Optional, TextIO, List, Sequence, Collection from pathlib import Path import numpy as np @@ -17,7 +17,7 @@ from . import util from . import grid_filters from . import Rotation from . import Table -from ._typehints import FloatSequence, IntSequence +from ._typehints import FloatSequence, IntSequence, IntCollection class Grid: """ @@ -907,7 +907,8 @@ class Grid: def clean(self, stencil: int = 3, - mutable: IntSequence = None, + selection: IntCollection = None, + invert_selection: bool = False, periodic: bool = True) -> 'Grid': """ Smooth grid by selecting most frequent material ID within given stencil at each location. @@ -916,8 +917,10 @@ class Grid: ---------- stencil : int, optional Size of smoothing stencil. Defaults to 3. - mutable : sequence of int, optional - Material ID that can be altered. Defaults to all. + selection : int or collection of int, optional + Material IDs to consider. + invert_selection : bool, optional + Consider all material IDs except those in selection. Defaults to False. periodic : bool, optional Assume grid to be periodic. Defaults to True. @@ -927,21 +930,23 @@ class Grid: Updated grid-based geometry. """ - def mostFrequent(arr: np.ndarray, mutable = None): + def mostFrequent(arr: np.ndarray, selection: List, invert: bool): me = arr[arr.size//2] - if selection is None or me in mutable: - unique, inverse = np.unique(arr, return_inverse=True) + if len(selection) == 0 or np.isin(me,selection,invert=invert): + unique, inverse = np.unique(arr,return_inverse=True) return unique[np.argmax(np.bincount(inverse))] else: return me - return Grid(material = ndimage.filters.generic_filter( - self.material, - mostFrequent, - size=(stencil if mutable is None else stencil//2*2+1,)*3, - mode=('wrap' if periodic else 'nearest'), - extra_keywords=dict(mutable=mutable), - ).astype(self.material.dtype), + extra_keywords = dict(selection=util.tbd(selection),invert=invert_selection) + material = ndimage.filters.generic_filter( + self.material, + mostFrequent, + size=(stencil if selection is None else stencil//2*2+1,)*3, + mode=('wrap' if periodic else 'nearest'), + extra_keywords=extra_keywords, + ).astype(self.material.dtype) + return Grid(material = material, size = self.size, origin = self.origin, comments = self.comments+[util.execution_stamp('Grid','clean')], @@ -1061,16 +1066,16 @@ class Grid: def substitute(self, - from_material: IntSequence, - to_material: IntSequence) -> 'Grid': + from_material: Union[int,IntSequence], + to_material: Union[int,IntSequence]) -> 'Grid': """ Substitute material indices. Parameters ---------- - from_material : sequence of int + from_material : int or sequence of int Material indices to be substituted. - to_material : sequence of int + to_material : int or sequence of int New material indices. Returns @@ -1080,7 +1085,8 @@ class Grid: """ material = self.material.copy() - for f,t in zip(from_material,to_material): # ToDo Python 3.10 has strict mode for zip + for f,t in zip(from_material if isinstance(from_material,(Sequence,np.ndarray)) else [from_material], + to_material if isinstance(to_material,(Sequence,np.ndarray)) else [to_material]): # ToDo Python 3.10 has strict mode for zip material[self.material==f] = t return Grid(material = material, @@ -1115,14 +1121,14 @@ class Grid: def vicinity_offset(self, vicinity: int = 1, offset: int = None, - trigger: IntSequence = [], + selection: IntCollection = None, + invert_selection: bool = False, periodic: bool = True) -> 'Grid': """ Offset material ID 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. - ToDo: use include/exclude as in seeds.from_grid Parameters ---------- @@ -1132,9 +1138,10 @@ class Grid: offset : int, optional Offset (positive or negative) to tag material indices, defaults to material.max()+1. - trigger : sequence of int, optional - List of material indices that trigger a change. - Defaults to [], meaning that any different neighbor triggers a change. + selection : int or collection of int, optional + Material IDs to that triger xxx. + invert_selection : bool, optional + Consider all material IDs except those in selection. Defaults to False. periodic : bool, optional Assume grid to be periodic. Defaults to True. @@ -1144,17 +1151,19 @@ class Grid: Updated grid-based geometry. """ - def tainted_neighborhood(stencil: np.ndarray, trigger): + def tainted_neighborhood(stencil: np.ndarray, selection): me = stencil[stencil.shape[0]//2] - return np.any(stencil != me if len(trigger) == 0 else - np.in1d(stencil,np.array(list(set(trigger) - {me})))) + return np.any(stencil != me if len(selection) == 0 else + np.in1d(stencil,np.array(list(set(selection) - {me})))) offset_ = np.nanmax(self.material)+1 if offset is None else offset + selection_ = util.tbd(selection) if not invert_selection else \ + list(set(self.material) - set(util.tbd(selection))) mask = ndimage.filters.generic_filter(self.material, tainted_neighborhood, size=1+2*vicinity, mode='wrap' if periodic else 'nearest', - extra_keywords={'trigger':trigger}) + extra_keywords=dict(selection=selection_)) return Grid(material = np.where(mask, self.material + offset_,self.material), size = self.size, diff --git a/python/damask/_typehints.py b/python/damask/_typehints.py index 5fcf39a41..f836e1f59 100644 --- a/python/damask/_typehints.py +++ b/python/damask/_typehints.py @@ -1,6 +1,6 @@ """Functionality for typehints.""" -from typing import Sequence, Union, Literal, TextIO +from typing import Sequence, Union, Literal, TextIO, Collection from pathlib import Path import numpy as np @@ -8,6 +8,7 @@ import numpy as np FloatSequence = Union[np.ndarray,Sequence[float]] IntSequence = Union[np.ndarray,Sequence[int]] +IntCollection = Union[np.ndarray,Collection[int]] FileHandle = Union[TextIO, str, Path] CrystalFamily = Union[None,Literal['triclinic', 'monoclinic', 'orthorhombic', 'tetragonal', 'hexagonal', 'cubic']] CrystalLattice = Union[None,Literal['aP', 'mP', 'mS', 'oP', 'oS', 'oI', 'oF', 'tP', 'tI', 'hP', 'cP', 'cI', 'cF']] diff --git a/python/damask/seeds.py b/python/damask/seeds.py index 620395f3c..d5e1ec7ed 100644 --- a/python/damask/seeds.py +++ b/python/damask/seeds.py @@ -6,7 +6,8 @@ from typing import Tuple as _Tuple from scipy import spatial as _spatial import numpy as _np -from ._typehints import FloatSequence as _FloatSequence, IntSequence as _IntSequence, NumpyRngSeed as _NumpyRngSeed +from ._typehints import FloatSequence as _FloatSequence, IntSequence as _IntSequence, \ + NumpyRngSeed as _NumpyRngSeed, IntCollection as _IntCollection from . import util as _util from . import grid_filters as _grid_filters @@ -106,7 +107,7 @@ def from_Poisson_disc(size: _FloatSequence, def from_grid(grid, - selection: _IntSequence = None, + selection: _IntCollection = None, invert_selection: bool = False, average: bool = False, periodic: bool = True) -> _Tuple[_np.ndarray, _np.ndarray]: @@ -117,7 +118,7 @@ def from_grid(grid, ---------- grid : damask.Grid Grid from which the material IDs are used as seeds. - selection : sequence of int, optional + selection : int or collection of int, optional Material IDs to consider. invert_selection : bool, optional Consider all material IDs except those in selection. Defaults to False. @@ -133,8 +134,9 @@ def from_grid(grid, """ material = grid.material.reshape((-1,1),order='F') - mask = _np.full(grid.cells.prod(),True,dtype=bool) if selection is None else \ - _np.isin(material,selection,invert=invert_selection).flatten() + selection_ = _util.tbd(selection) + mask = _np.full(grid.cells.prod(),True,dtype=bool) if len(selection_) == 0 else \ + _np.isin(material,selection_,invert=invert_selection).flatten() coords = _grid_filters.coordinates0_point(grid.cells,grid.size).reshape(-1,3,order='F') if not average: diff --git a/python/damask/util.py b/python/damask/util.py index 7ecb5ba54..55d1eb24c 100644 --- a/python/damask/util.py +++ b/python/damask/util.py @@ -9,7 +9,7 @@ import re import fractions from collections import abc from functools import reduce -from typing import Union, Tuple, Iterable, Callable, Dict, List, Any, Literal +from typing import Union, Tuple, Iterable, Callable, Dict, List, Any, Literal, Collection from pathlib import Path import numpy as np @@ -720,7 +720,13 @@ def dict_flatten(d: Dict) -> Dict: return new - +def tbd(arg) -> List: + if arg is None: + return [] + elif isinstance(arg,(np.ndarray,Collection)): + return list(arg) + else: + return [arg] #################################################################################################### # Classes diff --git a/python/tests/test_Grid.py b/python/tests/test_Grid.py index 6dd94f4bb..cfeb7be4a 100644 --- a/python/tests/test_Grid.py +++ b/python/tests/test_Grid.py @@ -155,7 +155,7 @@ class TestGrid: @pytest.mark.parametrize('selection',[None,[1],[1,2,3]]) @pytest.mark.parametrize('periodic',[True,False]) def test_clean(self,default,update,ref_path,stencil,selection,periodic): - current = default.clean(stencil,selection,periodic) + current = default.clean(stencil,selection,periodic=periodic) reference = ref_path/f'clean_{stencil}_{"+".join(map(str,[None] if selection is None else selection))}_{periodic}.vti' if update and stencil > 1: current.save(reference) @@ -296,8 +296,8 @@ class TestGrid: assert grid_equal(G_1,G_2) - @pytest.mark.parametrize('trigger',[[1],[]]) - def test_vicinity_offset(self,trigger): + @pytest.mark.parametrize('selection',[[1],[]]) + def test_vicinity_offset(self,selection): offset = np.random.randint(2,4) vicinity = np.random.randint(2,4) @@ -309,17 +309,17 @@ class TestGrid: for i in [0,1,2]: m2[(np.roll(m,+vicinity,i)-m)!=0] += offset m2[(np.roll(m,-vicinity,i)-m)!=0] += offset - if len(trigger) > 0: + if len(selection) > 0: m2[m==1] = 1 - grid = Grid(m,np.random.rand(3)).vicinity_offset(vicinity,offset,trigger=trigger) + grid = Grid(m,np.random.rand(3)).vicinity_offset(vicinity,offset,selection=selection) assert np.all(m2==grid.material) @pytest.mark.parametrize('periodic',[True,False]) def test_vicinity_offset_invariant(self,default,periodic): - offset = default.vicinity_offset(trigger=[default.material.max()+1, + offset = default.vicinity_offset(selection=[default.material.max()+1, default.material.min()-1]) assert np.all(offset.material==default.material)