Merge branch '250-configmaterial-material_add-simplifications' into 'development'

several improvements to ConfigMaterial and Config

Closes #250

See merge request damask/DAMASK!714
This commit is contained in:
Daniel Otto de Mentock 2023-02-06 16:52:14 +00:00
commit 4ae3274ac4
4 changed files with 157 additions and 64 deletions

View File

@ -2,6 +2,7 @@ import copy
from io import StringIO from io import StringIO
from collections.abc import Iterable from collections.abc import Iterable
import abc import abc
import platform
from typing import Optional, Union, Dict, Any, Type, TypeVar from typing import Optional, Union, Dict, Any, Type, TypeVar
import numpy as np import numpy as np
@ -69,13 +70,30 @@ class Config(dict):
**kwargs: arbitray keyword-value pairs, optional **kwargs: arbitray keyword-value pairs, optional
Top level entries of the configuration. Top level entries of the configuration.
Notes
-----
Values given as keyword-value pairs take precedence
over entries with the same keyword in 'config'.
""" """
if int(platform.python_version_tuple()[1]) >= 9:
if isinstance(config,str): if isinstance(config,str):
kwargs.update(yaml.load(config, Loader=SafeLoader)) kwargs = yaml.load(config, Loader=SafeLoader) | kwargs
elif isinstance(config,dict): elif isinstance(config,dict):
kwargs.update(config) kwargs = config | kwargs # type: ignore
super().__init__(**kwargs) super().__init__(**kwargs)
else:
if isinstance(config,str):
c = yaml.load(config, Loader=SafeLoader)
elif isinstance(config,dict):
c = config.copy()
else:
c = {}
c.update(kwargs)
super().__init__(**c)
def __repr__(self) -> str: def __repr__(self) -> str:
""" """

View File

@ -1,4 +1,4 @@
from typing import Optional, Union, Sequence, Dict, Any, Collection from typing import Optional, Union, Sequence, Dict, Any, List
import numpy as np import numpy as np
import h5py import h5py
@ -8,6 +8,7 @@ from . import Config
from . import Rotation from . import Rotation
from . import Orientation from . import Orientation
from . import util from . import util
from . import tensor
from . import Table from . import Table
@ -23,8 +24,10 @@ class ConfigMaterial(Config):
""" """
def __init__(self, def __init__(self,
config: Optional[Dict[str, Any]] = None, config: Optional[Union[str,Dict[str,Any]]] = None,*,
**kwargs): homogenization: Optional[Dict[str,Dict]] = None,
phase: Optional[Dict[str,Dict]] = None,
material: Optional[List[Dict[str,Any]]] = None):
""" """
New material configuration. New material configuration.
@ -32,16 +35,23 @@ class ConfigMaterial(Config):
---------- ----------
config : dict or str, optional config : dict or str, optional
Material configuration. String needs to be valid YAML. Material configuration. String needs to be valid YAML.
Defaults to None, in which case empty entries for homogenization : dict, optional
any missing material, homogenization, and phase entry are created. Homogenization configuration.
kwargs : key=value pairs, optional Defaults to an empty dict if 'config' is not given.
Initial content specified as pairs of key=value. phase : dict, optional
Phase configuration.
Defaults to an empty dict if 'config' is not given.
material : dict, optional
Materialpoint configuration.
Defaults to an empty list if 'config' is not given.
""" """
default: Collection kwargs: Dict[str,Union[Dict[str,Dict],List[Dict[str,Any]]]] = {}
if config is None: for arg,value in zip(['homogenization','phase','material'],[homogenization,phase,material]):
for section, default in {'material':[],'homogenization':{},'phase':{}}.items(): if value is None and config is None:
if section not in kwargs: kwargs.update({section:default}) kwargs[arg] = [] if arg == 'material' else {}
elif value is not None:
kwargs[arg] = value
super().__init__(config,**kwargs) super().__init__(config,**kwargs)
@ -170,8 +180,12 @@ class ConfigMaterial(Config):
@staticmethod @staticmethod
def from_table(table: Table, def from_table(table: Table,*,
**kwargs) -> 'ConfigMaterial': homogenization: Optional[Union[str,StrSequence]] = None,
phase: Optional[Union[str,StrSequence]] = None,
v: Optional[Union[str,FloatSequence]] = None,
O: Optional[Union[str,FloatSequence]] = None,
V_e: Optional[Union[str,FloatSequence]] = None) -> 'ConfigMaterial':
""" """
Generate from an ASCII table. Generate from an ASCII table.
@ -179,16 +193,33 @@ class ConfigMaterial(Config):
---------- ----------
table : damask.Table table : damask.Table
Table that contains material information. Table that contains material information.
**kwargs homogenization: (array-like) of str, optional
Keyword arguments where the key is the property name and Homogenization label.
the value specifies either the label of the data column in the table phase: (array-like) of str, optional
or a constant value. Phase label (per constituent).
v: (array-like) of float or str, optional
Constituent volume fraction (per constituent).
Defaults to 1/N_constituent.
O: (array-like) of damask.Rotation or np.array/list of shape(4) or str, optional
Orientation as unit quaternion (per constituent).
V_e: (array-like) of np.array/list of shape(3,3) or str, optional
Left elastic stretch (per constituent).
Returns Returns
------- -------
new : damask.ConfigMaterial new : damask.ConfigMaterial
Material configuration from values in table. Material configuration from values in table.
Notes
-----
If the value of an argument is a string that is a column label,
data from the table is used to fill the corresponding entry in
the material configuration. Otherwise, the value is used directly.
First index of array-like values that are defined per constituent
runs over materials, whereas second index runs over constituents.
Examples Examples
-------- --------
>>> import damask >>> import damask
@ -230,15 +261,16 @@ class ConfigMaterial(Config):
phase: {Aluminum: null, Steel: null} phase: {Aluminum: null, Steel: null}
""" """
kwargs_ = {k:table.get(v) if v in table.labels else np.atleast_2d([v]*len(table)).T for k,v in kwargs.items()} kwargs = {}
for arg,val in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
if val is not None:
kwargs[arg] = table.get(val) if val in table.labels else np.atleast_2d([val]*len(table)).T # type: ignore
_,idx = np.unique(np.hstack(list(kwargs_.values())),return_index=True,axis=0) _,idx = np.unique(np.hstack(list(kwargs.values())),return_index=True,axis=0)
idx = np.sort(idx) idx = np.sort(idx)
kwargs_ = {k:np.atleast_1d(v[idx].squeeze()) for k,v in kwargs_.items()} kwargs = {k:np.atleast_1d(v[idx].squeeze()) for k,v in kwargs.items()}
for what in ['phase','homogenization']:
if what not in kwargs_: kwargs_[what] = what+'_label'
return ConfigMaterial().material_add(**kwargs_) return ConfigMaterial().material_add(**kwargs)
@property @property
@ -434,7 +466,7 @@ class ConfigMaterial(Config):
Phase label (per constituent). Phase label (per constituent).
v: (array-like) of float, optional v: (array-like) of float, optional
Constituent volume fraction (per constituent). Constituent volume fraction (per constituent).
Defaults to 1/N_constituents Defaults to 1/N_constituent.
O: (array-like) of damask.Rotation or np.array/list of shape(4), optional O: (array-like) of damask.Rotation or np.array/list of shape(4), optional
Orientation as unit quaternion (per constituent). Orientation as unit quaternion (per constituent).
V_e: (array-like) of np.array/list of shape(3,3), optional V_e: (array-like) of np.array/list of shape(3,3), optional
@ -527,49 +559,48 @@ class ConfigMaterial(Config):
phase: {Austenite: null, Ferrite: null} phase: {Austenite: null, Ferrite: null}
""" """
kwargs = {} dim = {'O':(4,),'V_e':(3,3,)}
for keyword,value in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]): ex = dict((keyword, -len(val)) for keyword,val in dim.items())
if value is not None: kwargs[keyword] = value
_constituent_properties = ['phase','O','v','V_e']
_dim = {'O':(4,),'V_e':(3,3,)}
_ex = dict((k, -len(v)) for k, v in _dim.items())
N_materials,N_constituents = 1,1 N_materials,N_constituents = 1,1
shaped : Dict[str, Union[None,np.ndarray]] = \ shape = {}
{'v': None, for arg,val in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
'phase': None, if val is None: continue
'homogenization': None, shape[arg] = np.array(val)
} s = shape[arg].shape[:ex.get(arg,None)] # type: ignore
for arg,value in kwargs.items():
shaped[arg] = np.array(value)
s = shaped[arg].shape[:_ex.get(arg,None)] # type: ignore
N_materials = max(N_materials,s[0]) if len(s)>0 else N_materials N_materials = max(N_materials,s[0]) if len(s)>0 else N_materials
N_constituents = max(N_constituents,s[1]) if len(s)>1 else N_constituents N_constituents = max(N_constituents,s[1]) if len(s)>1 else N_constituents
shaped['v'] = np.array(1./N_constituents) if shaped['v'] is None else shaped['v'] shape['v'] = np.array(shape.get('v',1./N_constituents),float)
mat: Sequence[dict] = [{'constituents':[{} for _ in range(N_constituents)]} for _ in range(N_materials)] mat: Sequence[dict] = [{'constituents':[{} for _ in range(N_constituents)]} for _ in range(N_materials)]
for k,v in shaped.items(): for k,v in shape.items():
target = (N_materials,N_constituents) + _dim.get(k,()) target = (N_materials,N_constituents) + dim.get(k,())
obj = np.broadcast_to(np.array(v).reshape(util.shapeshifter(() if v is None else v.shape, broadcasted = np.broadcast_to(np.array(v).reshape(util.shapeshifter(np.array(v).shape,target,'right')),target)
target, if k == 'v':
mode = 'right')), if np.min(broadcasted) < 0 or np.max(broadcasted) > 1:
target) raise ValueError('volume fraction "v" out of range')
if len(np.atleast_1d(broadcasted)) > 1:
total = np.sum(broadcasted,axis=-1)
if np.min(total) < 0 or np.max(total) > 1:
raise ValueError('volume fraction "v" out of range')
if k == 'O' and not np.allclose(1.0,np.linalg.norm(broadcasted,axis=-1)):
raise ValueError('orientation "O" is not a unit quaterion')
elif k == 'V_e' and not np.allclose(broadcasted,tensor.symmetric(broadcasted)):
raise ValueError('elastic stretch "V_e" is not symmetric')
for i in range(N_materials): for i in range(N_materials):
if k in _constituent_properties: if k == 'homogenization':
for j in range(N_constituents): mat[i][k] = broadcasted[i,0]
mat[i]['constituents'][j][k] = obj[i,j].item() if isinstance(obj[i,j],np.generic) else obj[i,j]
else: else:
mat[i][k] = obj[i,0].item() if isinstance(obj[i,0],np.generic) else obj[i,0] for j in range(N_constituents):
mat[i]['constituents'][j][k] = broadcasted[i,j]
dup = self.copy() dup = self.copy()
dup['material'] = dup['material'] + mat if 'material' in dup else mat dup['material'] = dup['material'] + mat if 'material' in dup else mat
for what in [item for item in ['phase','homogenization'] if shaped[item] is not None]: for what in [item for item in ['phase','homogenization'] if item in shape]:
for k in np.unique(shaped[what]): # type: ignore for k in np.unique(shape[what]): # type: ignore
if k not in dup[what]: dup[what][str(k)] = None if k not in dup[what]: dup[what][str(k)] = None
return dup return dup

View File

@ -7,6 +7,17 @@ from damask import Orientation
class TestConfig: class TestConfig:
def test_init_keyword(self):
assert Config(p=4)['p'] == 4
@pytest.mark.parametrize('config',[{'p':1},'{p: 1}'])
def test_init_config(self,config):
assert Config(config)['p'] == 1
@pytest.mark.parametrize('config',[{'p':1},'{p: 1}'])
def test_init_both(self,config):
assert Config(config,p=2)['p'] == 2
@pytest.mark.parametrize('flow_style',[None,True,False]) @pytest.mark.parametrize('flow_style',[None,True,False])
def test_load_save_str(self,tmp_path,flow_style): def test_load_save_str(self,tmp_path,flow_style):
config = Config() config = Config()
@ -36,7 +47,6 @@ class TestConfig:
assert (config | Config(dummy)).delete({ 'hello':1,'foo':2 }) == config assert (config | Config(dummy)).delete({ 'hello':1,'foo':2 }) == config
assert (config | Config(dummy)).delete(Config({'hello':1 })) == config | {'foo':'bar'} assert (config | Config(dummy)).delete(Config({'hello':1 })) == config | {'foo':'bar'}
def test_repr(self,tmp_path): def test_repr(self,tmp_path):
config = Config() config = Config()
config['A'] = 1 config['A'] = 1

View File

@ -16,6 +16,27 @@ def ref_path(ref_path_base):
class TestConfigMaterial: class TestConfigMaterial:
def test_init_empty(self):
c = ConfigMaterial()
assert len(c) == 3
assert c['homogenization'] == {}
assert c['phase'] == {}
assert c['material'] == []
def test_init_d(self):
c = ConfigMaterial(config={'phase':4})
assert len(c) == 1
assert c['phase'] == 4
@pytest.mark.parametrize('kwargs',[{'homogenization':{'SX':{}}},
{'phase':{'Aluminum':{}}},
{'material':[{'A':1},{'B':2}]}])
def test_init_some(self,kwargs):
c = ConfigMaterial(**kwargs)
assert len(c) == 3
for k,v in kwargs.items():
if k in kwargs: assert v == kwargs[k]
@pytest.mark.parametrize('fname',[None,'test.yaml']) @pytest.mark.parametrize('fname',[None,'test.yaml'])
def test_load_save(self,ref_path,tmp_path,fname): def test_load_save(self,ref_path,tmp_path,fname):
reference = ConfigMaterial.load(ref_path/'material.yaml') reference = ConfigMaterial.load(ref_path/'material.yaml')
@ -88,14 +109,14 @@ class TestConfigMaterial:
def test_from_table(self): def test_from_table(self):
N = np.random.randint(3,10) N = np.random.randint(3,10)
a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])), a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])),
np.ones(N*2),np.zeros(N*2),np.ones(N*2),np.ones(N*2), np.zeros(N*2),np.ones(N*2),np.zeros(N*2),np.zeros(N*2),
np.ones(N*2), np.ones(N*2),
)).T )).T
t = Table({'varying':1,'constant':4,'ones':1},a) t = Table({'varying':1,'constant':4,'ones':1},a)
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'ones'}) c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'ones'})
assert len(c['material']) == N assert len(c['material']) == N
for i,m in enumerate(c['material']): for i,m in enumerate(c['material']):
assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [1,0,1,1]).all() assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [0,1,0,0]).all()
def test_updated_dicts(self,ref_path): def test_updated_dicts(self,ref_path):
m1 = ConfigMaterial().material_add(phase=['Aluminum'],O=[1.0,0.0,0.0,0.0],homogenization='SX') m1 = ConfigMaterial().material_add(phase=['Aluminum'],O=[1.0,0.0,0.0,0.0],homogenization='SX')
@ -109,14 +130,14 @@ class TestConfigMaterial:
def test_from_table_with_constant(self): def test_from_table_with_constant(self):
N = np.random.randint(3,10) N = np.random.randint(3,10)
a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])), a = np.vstack((np.hstack((np.arange(N),np.arange(N)[::-1])),
np.ones(N*2),np.zeros(N*2),np.ones(N*2),np.ones(N*2), np.zeros(N*2),np.ones(N*2),np.zeros(N*2),np.zeros(N*2),
np.ones(N*2), np.ones(N*2),
)).T )).T
t = Table({'varying':1,'constant':4,'ones':1},a) t = Table({'varying':1,'constant':4,'ones':1},a)
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':1}) c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':1})
assert len(c['material']) == N assert len(c['material']) == N
for i,m in enumerate(c['material']): for i,m in enumerate(c['material']):
assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [1,0,1,1]).all() assert m['homogenization'] == 1 and (m['constituents'][0]['O'] == [0,1,0,0]).all()
@pytest.mark.parametrize('N,n,kw',[ @pytest.mark.parametrize('N,n,kw',[
(1,1,{'phase':'Gold', (1,1,{'phase':'Gold',
@ -137,6 +158,19 @@ class TestConfigMaterial:
assert len(m['material']) == N assert len(m['material']) == N
assert len(m['material'][0]['constituents']) == n assert len(m['material'][0]['constituents']) == n
@pytest.mark.parametrize('shape',[(),(4,),(5,2)])
@pytest.mark.parametrize('kw',[{'V_e':np.random.rand(3,3)},
{'O':np.random.rand(4)},
{'v':np.array(2)}])
def test_material_add_invalid(self,kw,shape):
kw = {arg:np.broadcast_to(val,shape+val.shape) for arg,val in kw.items()}
with pytest.raises(ValueError):
ConfigMaterial().material_add(**kw)
@pytest.mark.parametrize('v',[2,np.ones(3)*2,np.ones((2,2))])
def test_material_add_invalid_v(self,v):
with pytest.raises(ValueError):
ConfigMaterial().material_add(v=v)
@pytest.mark.parametrize('cell_ensemble_data',[None,'CellEnsembleData']) @pytest.mark.parametrize('cell_ensemble_data',[None,'CellEnsembleData'])
def test_load_DREAM3D(self,ref_path,cell_ensemble_data): def test_load_DREAM3D(self,ref_path,cell_ensemble_data):