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:
commit
4ae3274ac4
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue