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 collections.abc import Iterable
import abc
import platform
from typing import Optional, Union, Dict, Any, Type, TypeVar
import numpy as np
@ -69,13 +70,30 @@ class Config(dict):
**kwargs: arbitray keyword-value pairs, optional
Top level entries of the configuration.
"""
if isinstance(config,str):
kwargs.update(yaml.load(config, Loader=SafeLoader))
elif isinstance(config,dict):
kwargs.update(config)
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):
kwargs = yaml.load(config, Loader=SafeLoader) | kwargs
elif isinstance(config,dict):
kwargs = config | kwargs # type: ignore
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)
super().__init__(**kwargs)
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 h5py
@ -8,6 +8,7 @@ from . import Config
from . import Rotation
from . import Orientation
from . import util
from . import tensor
from . import Table
@ -23,8 +24,10 @@ class ConfigMaterial(Config):
"""
def __init__(self,
config: Optional[Dict[str, Any]] = None,
**kwargs):
config: Optional[Union[str,Dict[str,Any]]] = None,*,
homogenization: Optional[Dict[str,Dict]] = None,
phase: Optional[Dict[str,Dict]] = None,
material: Optional[List[Dict[str,Any]]] = None):
"""
New material configuration.
@ -32,16 +35,23 @@ class ConfigMaterial(Config):
----------
config : dict or str, optional
Material configuration. String needs to be valid YAML.
Defaults to None, in which case empty entries for
any missing material, homogenization, and phase entry are created.
kwargs : key=value pairs, optional
Initial content specified as pairs of key=value.
homogenization : dict, optional
Homogenization configuration.
Defaults to an empty dict if 'config' is not given.
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
if config is None:
for section, default in {'material':[],'homogenization':{},'phase':{}}.items():
if section not in kwargs: kwargs.update({section:default})
kwargs: Dict[str,Union[Dict[str,Dict],List[Dict[str,Any]]]] = {}
for arg,value in zip(['homogenization','phase','material'],[homogenization,phase,material]):
if value is None and config is None:
kwargs[arg] = [] if arg == 'material' else {}
elif value is not None:
kwargs[arg] = value
super().__init__(config,**kwargs)
@ -170,8 +180,12 @@ class ConfigMaterial(Config):
@staticmethod
def from_table(table: Table,
**kwargs) -> 'ConfigMaterial':
def from_table(table: Table,*,
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.
@ -179,16 +193,33 @@ class ConfigMaterial(Config):
----------
table : damask.Table
Table that contains material information.
**kwargs
Keyword arguments where the key is the property name and
the value specifies either the label of the data column in the table
or a constant value.
homogenization: (array-like) of str, optional
Homogenization label.
phase: (array-like) of str, optional
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
-------
new : damask.ConfigMaterial
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
--------
>>> import damask
@ -230,15 +261,16 @@ class ConfigMaterial(Config):
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)
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'
kwargs = {k:np.atleast_1d(v[idx].squeeze()) for k,v in kwargs.items()}
return ConfigMaterial().material_add(**kwargs_)
return ConfigMaterial().material_add(**kwargs)
@property
@ -434,7 +466,7 @@ class ConfigMaterial(Config):
Phase label (per constituent).
v: (array-like) of float, optional
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
Orientation as unit quaternion (per constituent).
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}
"""
kwargs = {}
for keyword,value in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
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())
dim = {'O':(4,),'V_e':(3,3,)}
ex = dict((keyword, -len(val)) for keyword,val in dim.items())
N_materials,N_constituents = 1,1
shaped : Dict[str, Union[None,np.ndarray]] = \
{'v': None,
'phase': None,
'homogenization': None,
}
for arg,value in kwargs.items():
shaped[arg] = np.array(value)
s = shaped[arg].shape[:_ex.get(arg,None)] # type: ignore
shape = {}
for arg,val in zip(['homogenization','phase','v','O','V_e'],[homogenization,phase,v,O,V_e]):
if val is None: continue
shape[arg] = np.array(val)
s = shape[arg].shape[:ex.get(arg,None)] # type: ignore
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
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)]
for k,v in shaped.items():
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,
target,
mode = 'right')),
target)
for k,v in shape.items():
target = (N_materials,N_constituents) + dim.get(k,())
broadcasted = np.broadcast_to(np.array(v).reshape(util.shapeshifter(np.array(v).shape,target,'right')),target)
if k == 'v':
if np.min(broadcasted) < 0 or np.max(broadcasted) > 1:
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):
if k in _constituent_properties:
for j in range(N_constituents):
mat[i]['constituents'][j][k] = obj[i,j].item() if isinstance(obj[i,j],np.generic) else obj[i,j]
if k == 'homogenization':
mat[i][k] = broadcasted[i,0]
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['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 k in np.unique(shaped[what]): # type: ignore
for what in [item for item in ['phase','homogenization'] if item in shape]:
for k in np.unique(shape[what]): # type: ignore
if k not in dup[what]: dup[what][str(k)] = None
return dup

View File

@ -7,6 +7,17 @@ from damask import Orientation
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])
def test_load_save_str(self,tmp_path,flow_style):
config = Config()
@ -36,7 +47,6 @@ class TestConfig:
assert (config | Config(dummy)).delete({ 'hello':1,'foo':2 }) == config
assert (config | Config(dummy)).delete(Config({'hello':1 })) == config | {'foo':'bar'}
def test_repr(self,tmp_path):
config = Config()
config['A'] = 1

View File

@ -16,6 +16,27 @@ def ref_path(ref_path_base):
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'])
def test_load_save(self,ref_path,tmp_path,fname):
reference = ConfigMaterial.load(ref_path/'material.yaml')
@ -88,14 +109,14 @@ class TestConfigMaterial:
def test_from_table(self):
N = np.random.randint(3,10)
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),
)).T
t = Table({'varying':1,'constant':4,'ones':1},a)
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':'ones'})
assert len(c['material']) == N
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):
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):
N = np.random.randint(3,10)
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),
)).T
t = Table({'varying':1,'constant':4,'ones':1},a)
c = ConfigMaterial.from_table(t,**{'phase':'varying','O':'constant','homogenization':1})
assert len(c['material']) == N
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',[
(1,1,{'phase':'Gold',
@ -137,6 +158,19 @@ class TestConfigMaterial:
assert len(m['material']) == 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'])
def test_load_DREAM3D(self,ref_path,cell_ensemble_data):