DAMASK_EICMD/python/damask/_configmaterial.py

611 lines
23 KiB
Python

from typing import Optional, Union, Sequence, Dict, Any, List
import numpy as np
import h5py
from ._typehints import FileHandle, FloatSequence, StrSequence
from . import YAML
from . import Rotation
from . import Orientation
from . import util
from . import tensor
from . import Table
class ConfigMaterial(YAML):
"""
Material configuration.
Manipulate material configurations for storage in YAML format.
A complete material configuration file has the entries 'material',
'phase', and 'homogenization'. For use in DAMASK, it needs to be
stored as 'material.yaml'.
"""
def __init__(self,
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.
Parameters
----------
config : dict or str, optional
Material configuration. String needs to be valid YAML.
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.
"""
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)
def save(self,
fname: FileHandle = 'material.yaml',
**kwargs):
"""
Save to YAML file.
Parameters
----------
fname : file, str, or pathlib.Path, optional
Filename or file for writing. Defaults to 'material.yaml'.
**kwargs
Keyword arguments parsed to yaml.dump.
"""
super().save(fname,**kwargs)
@classmethod
def load(cls,
fname: FileHandle = 'material.yaml') -> 'ConfigMaterial':
"""
Load from YAML file.
Parameters
----------
fname : file, str, or pathlib.Path, optional
Filename or file to read from. Defaults to 'material.yaml'.
Returns
-------
loaded : damask.ConfigMaterial
Material configuration from file.
"""
return super(ConfigMaterial,cls).load(fname)
@staticmethod
def load_DREAM3D(fname: str,
grain_data: Optional[str] = None,
cell_data: Optional[str] = None,
cell_ensemble_data: str = 'CellEnsembleData',
phases: str = 'Phases',
Euler_angles: str = 'EulerAngles',
phase_names: str = 'PhaseName',
base_group: Optional[str] = None) -> 'ConfigMaterial':
"""
Load DREAM.3D (HDF5) file.
Data in DREAM.3D files can be stored per cell ('CellData')
and/or per grain ('Grain Data'). Per default, i.e. if
'grain_data' is None, cell-wise data is assumed.
Parameters
----------
fname : str or pathlib.Path
Filename of the DREAM.3D (HDF5) file.
grain_data : str
Name of the group (folder) containing grain-wise data. Defaults
to None, in which case cell-wise data is used.
cell_data : str
Name of the group (folder) containing cell-wise data. Defaults to
None in wich case it is automatically detected.
cell_ensemble_data : str
Name of the group (folder) containing data of cell ensembles. This
group is used to inquire the name of the phases. Phases will get
numeric IDs if this group is not found. Defaults to 'CellEnsembleData'.
phases : str
Name of the dataset containing the phase ID (cell-wise or grain-wise).
Defaults to 'Phases'.
Euler_angles : str
Name of the dataset containing the crystallographic orientation as
Euler angles in radians (cell-wise or grain-wise). Defaults to 'EulerAngles'.
phase_names : str
Name of the dataset containing the phase names. Phases will get
numeric IDs if this dataset is not found. Defaults to 'PhaseName'.
base_group : str
Path to the group (folder) that contains geometry (_SIMPL_GEOMETRY),
and grain- or cell-wise data. Defaults to None, in which case
it is set as the path that contains _SIMPL_GEOMETRY/SPACING.
Returns
-------
loaded : damask.ConfigMaterial
Material configuration from file.
Notes
-----
damask.GeomGrid.load_DREAM3D gives the corresponding geometry for
the grid solver.
For cell-wise data, only unique combinations of
orientation and phase are considered.
Homogenization and phase entries are emtpy and need to be
defined separately.
"""
with h5py.File(fname, 'r') as f:
b = util.DREAM3D_base_group(f) if base_group is None else base_group
c = util.DREAM3D_cell_data_group(f) if cell_data is None else cell_data
if grain_data is None:
phase = f['/'.join([b,c,phases])][()].flatten()
O = Rotation.from_Euler_angles(f['/'.join([b,c,Euler_angles])]).as_quaternion().reshape(-1,4) # noqa
_,idx = np.unique(np.hstack([O,phase.reshape(-1,1)]),return_index=True,axis=0)
idx = np.sort(idx)
else:
phase = f['/'.join([b,grain_data,phases])][()]
O = Rotation.from_Euler_angles(f['/'.join([b,grain_data,Euler_angles])]).as_quaternion() # noqa
idx = np.arange(phase.size)
if cell_ensemble_data is not None and phase_names is not None:
try:
names = np.array([s.decode() for s in f['/'.join([b,cell_ensemble_data,phase_names])]])
phase = names[phase]
except KeyError:
pass
base_config = ConfigMaterial({'phase':{k if isinstance(k,int) else str(k): None for k in np.unique(phase)},
'homogenization':{'direct':{'N_constituents':1}}})
constituent = {k:np.atleast_1d(v[idx].squeeze()) for k,v in zip(['O','phase'],[O,phase])}
return base_config.material_add(**constituent,homogenization='direct')
@staticmethod
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.
Parameters
----------
table : damask.Table
Table that contains material information.
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
>>> import damask.ConfigMaterial as cm
>>> t = damask.Table.load('small.txt')
>>> t
3:pos pos pos 4:qu qu qu qu phase homog
0 0 0 0 0.19 0.8 0.24 -0.51 Aluminum SX
1 1 0 0 0.8 0.19 0.24 -0.51 Steel SX
2 1 1 0 0.8 0.19 0.24 -0.51 Steel SX
>>> cm.from_table(t,O='qu',phase='phase',homogenization='homog')
material:
- constituents:
- O: [0.19, 0.8, 0.24, -0.51]
v: 1.0
phase: Aluminum
homogenization: SX
- constituents:
- O: [0.8, 0.19, 0.24, -0.51]
v: 1.0
phase: Steel
homogenization: SX
homogenization: {SX: null}
phase: {Aluminum: null, Steel: null}
>>> cm.from_table(t,O='qu',phase='phase',homogenization='single_crystal')
material:
- constituents:
- O: [0.19, 0.8, 0.24, -0.51]
v: 1.0
phase: Aluminum
homogenization: single_crystal
- constituents:
- O: [0.8, 0.19, 0.24, -0.51]
v: 1.0
phase: Steel
homogenization: single_crystal
homogenization: {single_crystal: null}
phase: {Aluminum: null, Steel: null}
"""
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.sort(idx)
kwargs = {k:np.atleast_1d(v[idx].squeeze()) for k,v in kwargs.items()}
return ConfigMaterial().material_add(**kwargs)
@property
def is_complete(self) -> bool:
"""
Check for completeness.
Only the general file layout is considered.
This check does not consider whether specific parameters for
a particular phase/homogenization model are missing.
Returns
-------
complete : bool
Whether the material.yaml definition is complete.
"""
def LabeledList(label,items):
return f'{label.capitalize()}{"s" if len(items)>1 else ""} {util.srepr(items,",",quote=True)}'
ok = True
msg = []
all = set(['homogenization','phase','material'])
miss = set([item for item in all if item not in self])
empty = set([item for item in all-miss if self[item] is None])
if miss:
msg.append(f'{LabeledList("top-level",miss)} missing')
ok = False
if empty:
msg.append(f'{LabeledList("top-level",empty)} empty')
if ok:
ok &= len(self['material']) > 0
if len(self['material']) < 1: msg.append('No materials defined')
homogenization = set()
phase = set()
for i,v in enumerate(self['material']):
if 'homogenization' in v:
homogenization.add(v['homogenization'])
else:
msg.append(f'No homogenization specified for material {i}')
ok = False
if 'constituents' in v:
for ii,vv in enumerate(v['constituents']):
if 'O' not in vv:
msg.append(f'No orientation specified for constituent {ii} of material {i}')
ok = False
if 'phase' in vv:
phase.add(vv['phase'])
else:
msg.append(f'No phase specified for constituent {ii} of material {i}')
ok = False
for v,other in {'phase':phase,
'homogenization':homogenization}.items():
me = set([] if v in empty else self[v])
if _miss := other - me:
msg.append(f'{LabeledList(v,_miss)} missing')
ok = False
if len(_empty := [item for item in me if self[v][item] is None]) > 0:
msg.append(f'{LabeledList(v,_empty)} undefined')
ok = False
print(util.srepr(msg))
return ok
@property
def is_valid(self) -> bool:
"""
Check for valid content.
Only the generic file content is considered.
This check does not consider whether parameters for a
particular phase/homogenization mode are out of bounds.
Returns
-------
valid : bool
Whether the material.yaml definition is valid.
"""
ok = True
if 'phase' in self:
for k,v in self['phase'].items():
if v is not None and 'lattice' in v:
try:
Orientation(lattice=v['lattice'])
except KeyError:
print(f"Invalid lattice '{v['lattice']}' in phase '{k}'")
ok = False
if 'material' in self:
for i,m in enumerate(self['material']):
if 'constituents' in m:
v = 0.0
for c in m['constituents']:
v += float(c['v'])
if 'O' in c:
try:
Rotation.from_quaternion(c['O'])
except ValueError:
print(f"Invalid orientation '{c['O']}' in material '{i}'")
ok = False
if not np.isclose(v,1.0):
print(f"Total fraction v = {v} ≠ 1 in material '{i}'")
ok = False
return ok
def material_rename_phase(self,
mapping: Dict[str, str],
ID: Optional[Sequence[int]] = None,
constituent: Optional[Sequence[int]] = None) -> 'ConfigMaterial':
"""
Change phase name in material.
Parameters
----------
mapping: dict
Mapping from old name to new name
ID: list of ints, optional
Limit renaming to selected material IDs.
constituent: list of ints, optional
Limit renaming to selected constituents.
Returns
-------
updated : damask.ConfigMaterial
Updated material configuration.
"""
dup = self.copy()
for i,m in enumerate(dup['material']):
if ID is not None and i not in ID: continue
for c in m['constituents']:
if constituent is not None and c not in constituent: continue
try:
c['phase'] = mapping[c['phase']]
except KeyError:
continue
return dup
def material_rename_homogenization(self,
mapping: Dict[str, str],
ID: Optional[Sequence[int]] = None) -> 'ConfigMaterial':
"""
Change homogenization name in material.
Parameters
----------
mapping: dict
Mapping from old name to new name
ID: list of ints, optional
Limit renaming to selected homogenization IDs.
Returns
-------
updated : damask.ConfigMaterial
Updated material configuration.
"""
dup = self.copy()
for i,m in enumerate(dup['material']):
if ID is not None and i not in ID: continue
try:
m['homogenization'] = mapping[m['homogenization']]
except KeyError:
continue
return dup
def material_add(self,*,
homogenization: Optional[Union[str,StrSequence]] = None,
phase: Optional[Union[str,StrSequence]] = None,
v: Optional[Union[float,FloatSequence]] = None,
O: Optional[Union[float,FloatSequence]] = None,
V_e: Optional[Union[float,FloatSequence]] = None) -> 'ConfigMaterial':
"""
Add material entries.
Parameters
----------
homogenization: (array-like) of str, optional
Homogenization label.
phase: (array-like) of str, optional
Phase label (per constituent).
v: (array-like) of float, optional
Constituent volume fraction (per constituent).
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
Left elastic stretch (per constituent).
Returns
-------
updated : damask.ConfigMaterial
Updated material configuration.
Notes
-----
First index of array-like values that are defined per constituent
runs over materials, whereas second index runs over constituents.
Examples
--------
Create two grains of ferrite and one grain of martensite, each with random orientation:
>>> import damask
>>> m = damask.ConfigMaterial()
>>> m = m.material_add(phase = ['Ferrite','Martensite','Ferrite'],
... O = damask.Rotation.from_random(3),
... homogenization = 'SX')
>>> m
material:
- constituents:
- O: [0.577764, -0.146299, -0.617669, 0.513010]
v: 1.0
phase: Ferrite
homogenization: SX
- constituents:
- O: [0.184176, 0.340305, 0.737247, 0.553840]
v: 1.0
phase: Martensite
homogenization: SX
- constituents:
- O: [0.47925185, -0.04294454, 0.78760173, -0.3849116 ]
v: 1.0
phase: Ferrite
homogenization: SX
homogenization: {SX: null}
phase: {Ferrite: null, Martensite: null}
Create hundred materials that each approximate a duplex stainless steel microstructure
with three austenite and one relatively bigger ferrite grain of random orientation each:
>>> import damask
>>> m = damask.ConfigMaterial()
>>> m = m.material_add(phase = np.array(['Austenite']*3+['Ferrite']),
... O = damask.Rotation.from_random((100,4)),
... v = np.array([0.2]*3+[0.4]),
... homogenization = 'Taylor')
>>> m
material:
- constituents:
- v: 0.2
phase: Austenite
O: [0.46183665006602664, 0.2215160420973196, -0.5594313187331139, 0.6516702781083836]
- v: 0.2
phase: Austenite
O: [0.11321658382410027, 0.6354079414360444, 0.00562701344273936, 0.7638108992590535]
- v: 0.2
phase: Austenite
O: [0.050991978809077604, 0.8069522034362003, -0.11352928955610851, -0.5773552285027659]
- v: 0.4
phase: Ferrite
O: [0.9460076150721788, 0.15880754622367604, -0.0069841062241482385, -0.28249066842661014]
homogenization: Taylor
.
.
.
- constituents:
- v: 0.2
phase: Austenite
O: [0.12531400788494199, -0.18637769037997565, 0.31737548053338394, -0.9213210951197429]
- v: 0.2
phase: Austenite
O: [0.37453930577161404, -0.33529507696450805, -0.3266564259130028, -0.800370601162502]
- v: 0.2
phase: Austenite
O: [0.035776891752713764, -0.720706371010592, -0.4540438656728926, -0.5226342017569017]
- v: 0.4
phase: Ferrite
O: [0.6782596727966124, -0.20800082041703685, -0.138636083554039, 0.6909989227925536]
homogenization: Taylor
homogenization: {Taylor: null}
phase: {Austenite: null, Ferrite: null}
"""
dim = {'O':(4,),'V_e':(3,3,)}
ex = dict((keyword, -len(val)) for keyword,val in dim.items())
N_materials,N_constituents = 1,1
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
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 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 == 'homogenization':
mat[i][k] = broadcasted[i,0]
else:
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 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