clarify out-of-place behavior and document return values

This commit is contained in:
Martin Diehl 2021-04-23 19:20:07 +02:00
parent 26f37d822d
commit 4d67c85a33
8 changed files with 324 additions and 142 deletions

@ -1 +1 @@
Subproject commit 23eac08c9f9638f8dae76710095222d00f948eec Subproject commit d58a002b0a43d240f143aee1396fdc766d87a886

View File

@ -60,54 +60,6 @@ class ConfigMaterial(Config):
return super(ConfigMaterial,cls).load(fname) return super(ConfigMaterial,cls).load(fname)
@staticmethod
def from_table(table,**kwargs):
"""
Generate from an ASCII table.
Parameters
----------
table : damask.Table
Table that contains material information.
**kwargs
Keyword arguments where the key is the name and the value specifies
the label of the data column in the table.
Examples
--------
>>> import damask
>>> import damask.ConfigMaterial as cm
>>> t = damask.Table.load('small.txt')
>>> t
pos pos pos 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
1 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: {}
phase: {}
"""
kwargs_ = {k:table.get(v) for k,v in kwargs.items()}
_,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_)
@staticmethod @staticmethod
def load_DREAM3D(fname, def load_DREAM3D(fname,
grain_data=None,cell_data=None,cell_ensemble_data='CellEnsembleData', grain_data=None,cell_data=None,cell_ensemble_data='CellEnsembleData',
@ -181,9 +133,69 @@ class ConfigMaterial(Config):
return base_config.material_add(**constituent,homogenization='direct') return base_config.material_add(**constituent,homogenization='direct')
@staticmethod
def from_table(table,**kwargs):
"""
Generate from an ASCII table.
Parameters
----------
table : damask.Table
Table that contains material information.
**kwargs
Keyword arguments where the key is the name and the value specifies
the label of the data column in the table.
Examples
--------
>>> import damask
>>> import damask.ConfigMaterial as cm
>>> t = damask.Table.load('small.txt')
>>> t
pos pos pos 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
1 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: {}
phase: {}
"""
kwargs_ = {k:table.get(v) for k,v in kwargs.items()}
_,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 @property
def is_complete(self): def is_complete(self):
"""Check for completeness.""" """
Check for completeness.
Only the general file layout is considered.
This check does not consider whether parameters for
a particular phase/homogenization model are missing.
Returns
-------
complete : bool
Whether the material.yaml definition is complete.
"""
ok = True ok = True
for top_level in ['homogenization','phase','material']: for top_level in ['homogenization','phase','material']:
ok &= top_level in self ok &= top_level in self
@ -236,7 +248,19 @@ class ConfigMaterial(Config):
@property @property
def is_valid(self): def is_valid(self):
"""Check for valid content.""" """
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 ok = True
if 'phase' in self: if 'phase' in self:
@ -282,7 +306,7 @@ class ConfigMaterial(Config):
Returns Returns
------- -------
cfg : damask.ConfigMaterial updated : damask.ConfigMaterial
Updated material configuration. Updated material configuration.
""" """
@ -311,7 +335,7 @@ class ConfigMaterial(Config):
Returns Returns
------- -------
cfg : damask.ConfigMaterial updated : damask.ConfigMaterial
Updated material configuration. Updated material configuration.
""" """
@ -336,7 +360,7 @@ class ConfigMaterial(Config):
Returns Returns
------- -------
cfg : damask.ConfigMaterial updated : damask.ConfigMaterial
Updated material configuration. Updated material configuration.
Examples Examples

View File

@ -57,11 +57,15 @@ def _empty_like(dataset,N_materialpoints,fill_float,fill_int):
class Result: class Result:
""" """
Manipulate and read DADF5 files. Add data to and export from DADF5 files.
DADF5 (DAMASK HDF5) files contain DAMASK results. DADF5 (DAMASK HDF5) files contain DAMASK results.
The group/folder structure reflects the input data Their group/folder structure reflects the input data in material.yaml.
in material.yaml.
This class provides a custom view on the DADF5 file.
Upon initialization, all attributes are visible.
Derived quantities can be added to the file and existing data can be exported based on the current view.
""" """
def __init__(self,fname): def __init__(self,fname):
@ -274,6 +278,11 @@ class Result:
Name of datasets; supports '?' and '*' wildcards. Name of datasets; supports '?' and '*' wildcards.
True is equivalent to '*', False is equivalent to []. True is equivalent to '*', False is equivalent to [].
Returns
-------
view : damask.Result
View with where selected attributes are visible.
""" """
return self._manage_view('set',what,datasets) return self._manage_view('set',what,datasets)
@ -290,6 +299,11 @@ class Result:
Name of datasets; supports '?' and '*' wildcards. Name of datasets; supports '?' and '*' wildcards.
True is equivalent to '*', False is equivalent to []. True is equivalent to '*', False is equivalent to [].
Returns
-------
modified_view : damask.Result
View with more visible attributes.
""" """
return self._manage_view('add',what,datasets) return self._manage_view('add',what,datasets)
@ -306,6 +320,11 @@ class Result:
Name of datasets; supports '?' and '*' wildcards. Name of datasets; supports '?' and '*' wildcards.
True is equivalent to '*', False is equivalent to []. True is equivalent to '*', False is equivalent to [].
Returns
-------
modified_view : damask.Result
View with less visible attributes.
""" """
return self._manage_view('del',what,datasets) return self._manage_view('del',what,datasets)
@ -372,7 +391,7 @@ class Result:
@property @property
def coordinates0_point(self): def coordinates0_point(self):
"""Return initial coordinates of the cell centers.""" """Initial/undeformed cell center coordinates."""
if self.structured: if self.structured:
return grid_filters.coordinates0_point(self.cells,self.size,self.origin).reshape(-1,3,order='F') return grid_filters.coordinates0_point(self.cells,self.size,self.origin).reshape(-1,3,order='F')
else: else:
@ -381,7 +400,7 @@ class Result:
@property @property
def coordinates0_node(self): def coordinates0_node(self):
"""Return initial coordinates of the cell centers.""" """Initial/undeformed nodal coordinates."""
if self.structured: if self.structured:
return grid_filters.coordinates0_node(self.cells,self.size,self.origin).reshape(-1,3,order='F') return grid_filters.coordinates0_node(self.cells,self.size,self.origin).reshape(-1,3,order='F')
else: else:
@ -390,6 +409,7 @@ class Result:
@property @property
def geometry0(self): def geometry0(self):
"""Initial/undeformed geometry."""
if self.structured: if self.structured:
return VTK.from_rectilinear_grid(self.cells,self.size,self.origin) return VTK.from_rectilinear_grid(self.cells,self.size,self.origin)
else: else:

View File

@ -223,6 +223,11 @@ class Table:
fname : file, str, or pathlib.Path fname : file, str, or pathlib.Path
Filename or file for reading. Filename or file for reading.
Returns
-------
loaded : damask.Table
Table data from file.
""" """
try: try:
f = open(fname) f = open(fname)
@ -275,6 +280,11 @@ class Table:
fname : file, str, or pathlib.Path fname : file, str, or pathlib.Path
Filename or file for reading. Filename or file for reading.
Returns
-------
loaded : damask.Table
Table data from file.
""" """
try: try:
f = open(fname) f = open(fname)
@ -334,14 +344,14 @@ class Table:
---------- ----------
label : str label : str
Column label. Column label.
data : np.ndarray data : numpy.ndarray
New data. New data.
info : str, optional info : str, optional
Human-readable information about the new data. Human-readable information about the new data.
Returns Returns
------- -------
table : Table updated : damask.Table
Updated table. Updated table.
""" """
@ -367,14 +377,14 @@ class Table:
---------- ----------
label : str label : str
Column label. Column label.
data : np.ndarray data : numpy.ndarray
Modified data. Modified data.
info : str, optional info : str, optional
Human-readable information about the modified data. Human-readable information about the modified data.
Returns Returns
------- -------
table : Table udated : damask.Table
Updated table. Updated table.
""" """
@ -402,7 +412,7 @@ class Table:
Returns Returns
------- -------
table : Table udated : damask.Table
Updated table. Updated table.
""" """
@ -425,7 +435,7 @@ class Table:
Returns Returns
------- -------
table : Table udated : damask.Table
Updated table. Updated table.
""" """
@ -451,7 +461,7 @@ class Table:
Returns Returns
------- -------
table : Table udated : damask.Table
Updated table. Updated table.
""" """
@ -479,13 +489,13 @@ class Table:
Parameters Parameters
---------- ----------
other : Table other : damask.Table
Table to append. Table to append.
Returns Returns
------- -------
table : Table udated : damask.Table
Concatenated table. Updated table.
""" """
if self.shapes != other.shapes or not self.data.columns.equals(other.data.columns): if self.shapes != other.shapes or not self.data.columns.equals(other.data.columns):
@ -504,13 +514,13 @@ class Table:
Parameters Parameters
---------- ----------
other : Table other : damask.Table
Table to join. Table to join.
Returns Returns
------- -------
table : Table udated : damask.Table
Joined table. Updated table.
""" """
if set(self.shapes) & set(other.shapes) or self.data.shape[0] != other.data.shape[0]: if set(self.shapes) & set(other.shapes) or self.data.shape[0] != other.data.shape[0]:

View File

@ -5,11 +5,12 @@ Notes
----- -----
The grids are defined as (x,y,z,...) where x is fastest and z is slowest. The grids are defined as (x,y,z,...) where x is fastest and z is slowest.
This convention is consistent with the layout in grid vtr files. This convention is consistent with the layout in grid vtr files.
When converting to/from a plain list (e.g. storage in ASCII table), When converting to/from a plain list (e.g. storage in ASCII table),
the following operations are required for tensorial data: the following operations are required for tensorial data:
D3 = D1.reshape(cells+(-1,),order='F').reshape(cells+(3,3)) - D3 = D1.reshape(cells+(-1,),order='F').reshape(cells+(3,3))
D1 = D3.reshape(cells+(-1,)).reshape(-1,9,order='F') - D1 = D3.reshape(cells+(-1,)).reshape(-1,9,order='F')
""" """
from scipy import spatial as _spatial from scipy import spatial as _spatial
@ -29,10 +30,12 @@ def _ks(size,cells,first_order=False):
Correction for first order derivatives, defaults to False. Correction for first order derivatives, defaults to False.
""" """
k_sk = _np.where(_np.arange(cells[0])>cells[0]//2,_np.arange(cells[0])-cells[0],_np.arange(cells[0]))/size[0] k_sk = _np.where(_np.arange(cells[0])>cells[0]//2,
_np.arange(cells[0])-cells[0],_np.arange(cells[0]))/size[0]
if cells[0]%2 == 0 and first_order: k_sk[cells[0]//2] = 0 # Nyquist freq=0 for even cells (Johnson, MIT, 2011) if cells[0]%2 == 0 and first_order: k_sk[cells[0]//2] = 0 # Nyquist freq=0 for even cells (Johnson, MIT, 2011)
k_sj = _np.where(_np.arange(cells[1])>cells[1]//2,_np.arange(cells[1])-cells[1],_np.arange(cells[1]))/size[1] k_sj = _np.where(_np.arange(cells[1])>cells[1]//2,
_np.arange(cells[1])-cells[1],_np.arange(cells[1]))/size[1]
if cells[1]%2 == 0 and first_order: k_sj[cells[1]//2] = 0 # Nyquist freq=0 for even cells (Johnson, MIT, 2011) if cells[1]%2 == 0 and first_order: k_sj[cells[1]//2] = 0 # Nyquist freq=0 for even cells (Johnson, MIT, 2011)
k_si = _np.arange(cells[2]//2+1)/size[2] k_si = _np.arange(cells[2]//2+1)/size[2]
@ -40,74 +43,89 @@ def _ks(size,cells,first_order=False):
return _np.stack(_np.meshgrid(k_sk,k_sj,k_si,indexing = 'ij'), axis=-1) return _np.stack(_np.meshgrid(k_sk,k_sj,k_si,indexing = 'ij'), axis=-1)
def curl(size,field): def curl(size,f):
""" u"""
Calculate curl of a vector or tensor field in Fourier space. Calculate curl of a vector or tensor field in Fourier space.
Parameters Parameters
---------- ----------
size : numpy.ndarray of shape (3) size : numpy.ndarray of shape (3)
Physical size of the periodic field. Physical size of the periodic field.
field : numpy.ndarray of shape (:,:,:,3) or (:,:,:,3,3) f : numpy.ndarray of shape (:,:,:,3) or (:,:,:,3,3)
Periodic field of which the curl is calculated. Periodic field of which the curl is calculated.
Returns
-------
× f : numpy.ndarray
Curl of f.
""" """
n = _np.prod(field.shape[3:]) n = _np.prod(f.shape[3:])
k_s = _ks(size,field.shape[:3],True) k_s = _ks(size,f.shape[:3],True)
e = _np.zeros((3, 3, 3)) e = _np.zeros((3, 3, 3))
e[0, 1, 2] = e[1, 2, 0] = e[2, 0, 1] = +1.0 # Levi-Civita symbol e[0, 1, 2] = e[1, 2, 0] = e[2, 0, 1] = +1.0 # Levi-Civita symbol
e[0, 2, 1] = e[2, 1, 0] = e[1, 0, 2] = -1.0 e[0, 2, 1] = e[2, 1, 0] = e[1, 0, 2] = -1.0
field_fourier = _np.fft.rfftn(field,axes=(0,1,2)) f_fourier = _np.fft.rfftn(f,axes=(0,1,2))
curl_ = (_np.einsum('slm,ijkl,ijkm ->ijks', e,k_s,field_fourier)*2.0j*_np.pi if n == 3 else # vector, 3 -> 3 curl_ = (_np.einsum('slm,ijkl,ijkm ->ijks', e,k_s,f_fourier)*2.0j*_np.pi if n == 3 else # vector, 3 -> 3
_np.einsum('slm,ijkl,ijknm->ijksn',e,k_s,field_fourier)*2.0j*_np.pi) # tensor, 3x3 -> 3x3 _np.einsum('slm,ijkl,ijknm->ijksn',e,k_s,f_fourier)*2.0j*_np.pi) # tensor, 3x3 -> 3x3
return _np.fft.irfftn(curl_,axes=(0,1,2),s=field.shape[:3]) return _np.fft.irfftn(curl_,axes=(0,1,2),s=f.shape[:3])
def divergence(size,field): def divergence(size,f):
""" u"""
Calculate divergence of a vector or tensor field in Fourier space. Calculate divergence of a vector or tensor field in Fourier space.
Parameters Parameters
---------- ----------
size : numpy.ndarray of shape (3) size : numpy.ndarray of shape (3)
Physical size of the periodic field. Physical size of the periodic field.
field : numpy.ndarray of shape (:,:,:,3) or (:,:,:,3,3) f : numpy.ndarray of shape (:,:,:,3) or (:,:,:,3,3)
Periodic field of which the divergence is calculated. Periodic field of which the divergence is calculated.
Returns
-------
· f : numpy.ndarray
Divergence of f.
""" """
n = _np.prod(field.shape[3:]) n = _np.prod(f.shape[3:])
k_s = _ks(size,field.shape[:3],True) k_s = _ks(size,f.shape[:3],True)
field_fourier = _np.fft.rfftn(field,axes=(0,1,2)) f_fourier = _np.fft.rfftn(f,axes=(0,1,2))
div_ = (_np.einsum('ijkl,ijkl ->ijk', k_s,field_fourier)*2.0j*_np.pi if n == 3 else # vector, 3 -> 1 div_ = (_np.einsum('ijkl,ijkl ->ijk', k_s,f_fourier)*2.0j*_np.pi if n == 3 else # vector, 3 -> 1
_np.einsum('ijkm,ijklm->ijkl',k_s,field_fourier)*2.0j*_np.pi) # tensor, 3x3 -> 3 _np.einsum('ijkm,ijklm->ijkl',k_s,f_fourier)*2.0j*_np.pi) # tensor, 3x3 -> 3
return _np.fft.irfftn(div_,axes=(0,1,2),s=field.shape[:3]) return _np.fft.irfftn(div_,axes=(0,1,2),s=f.shape[:3])
def gradient(size,field): def gradient(size,f):
""" u"""
Calculate gradient of a scalar or vector field in Fourier space. Calculate gradient of a scalar or vector fieldin Fourier space.
Parameters Parameters
---------- ----------
size : numpy.ndarray of shape (3) size : numpy.ndarray of shape (3)
Physical size of the periodic field. Physical size of the periodic field.
field : numpy.ndarray of shape (:,:,:,1) or (:,:,:,3) f : numpy.ndarray of shape (:,:,:,1) or (:,:,:,3)
Periodic field of which the gradient is calculated. Periodic field of which the gradient is calculated.
Returns
-------
f : numpy.ndarray
Divergence of f.
""" """
n = _np.prod(field.shape[3:]) n = _np.prod(f.shape[3:])
k_s = _ks(size,field.shape[:3],True) k_s = _ks(size,f.shape[:3],True)
field_fourier = _np.fft.rfftn(field,axes=(0,1,2)) f_fourier = _np.fft.rfftn(f,axes=(0,1,2))
grad_ = (_np.einsum('ijkl,ijkm->ijkm', field_fourier,k_s)*2.0j*_np.pi if n == 1 else # scalar, 1 -> 3 grad_ = (_np.einsum('ijkl,ijkm->ijkm', f_fourier,k_s)*2.0j*_np.pi if n == 1 else # scalar, 1 -> 3
_np.einsum('ijkl,ijkm->ijklm',field_fourier,k_s)*2.0j*_np.pi) # vector, 3 -> 3x3 _np.einsum('ijkl,ijkm->ijklm',f_fourier,k_s)*2.0j*_np.pi) # vector, 3 -> 3x3
return _np.fft.irfftn(grad_,axes=(0,1,2),s=field.shape[:3]) return _np.fft.irfftn(grad_,axes=(0,1,2),s=f.shape[:3])
def coordinates0_point(cells,size,origin=_np.zeros(3)): def coordinates0_point(cells,size,origin=_np.zeros(3)):
@ -123,6 +141,11 @@ def coordinates0_point(cells,size,origin=_np.zeros(3)):
origin : numpy.ndarray, optional origin : numpy.ndarray, optional
Physical origin of the periodic field. Defaults to [0.0,0.0,0.0]. Physical origin of the periodic field. Defaults to [0.0,0.0,0.0].
Returns
-------
x_p_0 : numpy.ndarray
Undeformed cell center coordinates.
""" """
start = origin + size/cells*.5 start = origin + size/cells*.5
end = origin + size - size/cells*.5 end = origin + size - size/cells*.5
@ -144,6 +167,11 @@ def displacement_fluct_point(size,F):
F : numpy.ndarray F : numpy.ndarray
Deformation gradient field. Deformation gradient field.
Returns
-------
u_p_fluct : numpy.ndarray
Fluctuating part of the cell center displacements.
""" """
integrator = 0.5j*size/_np.pi integrator = 0.5j*size/_np.pi
@ -171,6 +199,11 @@ def displacement_avg_point(size,F):
F : numpy.ndarray F : numpy.ndarray
Deformation gradient field. Deformation gradient field.
Returns
-------
u_p_avg : numpy.ndarray
Average part of the cell center displacements.
""" """
F_avg = _np.average(F,axis=(0,1,2)) F_avg = _np.average(F,axis=(0,1,2))
return _np.einsum('ml,ijkl->ijkm',F_avg - _np.eye(3),coordinates0_point(F.shape[:3],size)) return _np.einsum('ml,ijkl->ijkm',F_avg - _np.eye(3),coordinates0_point(F.shape[:3],size))
@ -187,6 +220,11 @@ def displacement_point(size,F):
F : numpy.ndarray F : numpy.ndarray
Deformation gradient field. Deformation gradient field.
Returns
-------
u_p : numpy.ndarray
Cell center displacements.
""" """
return displacement_avg_point(size,F) + displacement_fluct_point(size,F) return displacement_avg_point(size,F) + displacement_fluct_point(size,F)
@ -204,6 +242,11 @@ def coordinates_point(size,F,origin=_np.zeros(3)):
origin : numpy.ndarray of shape (3), optional origin : numpy.ndarray of shape (3), optional
Physical origin of the periodic field. Defaults to [0.0,0.0,0.0]. Physical origin of the periodic field. Defaults to [0.0,0.0,0.0].
Returns
-------
x_p : numpy.ndarray
Cell center coordinates.
""" """
return coordinates0_point(F.shape[:3],size,origin) + displacement_point(size,F) return coordinates0_point(F.shape[:3],size,origin) + displacement_point(size,F)
@ -252,19 +295,6 @@ def cellsSizeOrigin_coordinates0_point(coordinates0,ordered=True):
return (cells,size,origin) return (cells,size,origin)
def coordinates0_check(coordinates0):
"""
Check whether coordinates lie on a regular grid.
Parameters
----------
coordinates0 : numpy.ndarray
Array of undeformed cell coordinates.
"""
cellsSizeOrigin_coordinates0_point(coordinates0,ordered=True)
def coordinates0_node(cells,size,origin=_np.zeros(3)): def coordinates0_node(cells,size,origin=_np.zeros(3)):
""" """
Nodal positions (undeformed). Nodal positions (undeformed).
@ -278,6 +308,11 @@ def coordinates0_node(cells,size,origin=_np.zeros(3)):
origin : numpy.ndarray of shape (3), optional origin : numpy.ndarray of shape (3), optional
Physical origin of the periodic field. Defaults to [0.0,0.0,0.0]. Physical origin of the periodic field. Defaults to [0.0,0.0,0.0].
Returns
-------
x_n_0 : numpy.ndarray
Undeformed nodal coordinates.
""" """
return _np.stack(_np.meshgrid(_np.linspace(origin[0],size[0]+origin[0],cells[0]+1), return _np.stack(_np.meshgrid(_np.linspace(origin[0],size[0]+origin[0],cells[0]+1),
_np.linspace(origin[1],size[1]+origin[1],cells[1]+1), _np.linspace(origin[1],size[1]+origin[1],cells[1]+1),
@ -296,6 +331,11 @@ def displacement_fluct_node(size,F):
F : numpy.ndarray F : numpy.ndarray
Deformation gradient field. Deformation gradient field.
Returns
-------
u_n_fluct : numpy.ndarray
Fluctuating part of the nodal displacements.
""" """
return point_to_node(displacement_fluct_point(size,F)) return point_to_node(displacement_fluct_point(size,F))
@ -311,6 +351,11 @@ def displacement_avg_node(size,F):
F : numpy.ndarray F : numpy.ndarray
Deformation gradient field. Deformation gradient field.
Returns
-------
u_n_avg : numpy.ndarray
Average part of the nodal displacements.
""" """
F_avg = _np.average(F,axis=(0,1,2)) F_avg = _np.average(F,axis=(0,1,2))
return _np.einsum('ml,ijkl->ijkm',F_avg - _np.eye(3),coordinates0_node(F.shape[:3],size)) return _np.einsum('ml,ijkl->ijkm',F_avg - _np.eye(3),coordinates0_node(F.shape[:3],size))
@ -327,6 +372,11 @@ def displacement_node(size,F):
F : numpy.ndarray F : numpy.ndarray
Deformation gradient field. Deformation gradient field.
Returns
-------
u_p : numpy.ndarray
Nodal displacements.
""" """
return displacement_avg_node(size,F) + displacement_fluct_node(size,F) return displacement_avg_node(size,F) + displacement_fluct_node(size,F)
@ -344,28 +394,15 @@ def coordinates_node(size,F,origin=_np.zeros(3)):
origin : numpy.ndarray of shape (3), optional origin : numpy.ndarray of shape (3), optional
Physical origin of the periodic field. Defaults to [0.0,0.0,0.0]. Physical origin of the periodic field. Defaults to [0.0,0.0,0.0].
Returns
-------
x_n : numpy.ndarray
Nodal coordinates.
""" """
return coordinates0_node(F.shape[:3],size,origin) + displacement_node(size,F) return coordinates0_node(F.shape[:3],size,origin) + displacement_node(size,F)
def point_to_node(cell_data):
"""Interpolate periodic point data to nodal data."""
n = ( cell_data + _np.roll(cell_data,1,(0,1,2))
+ _np.roll(cell_data,1,(0,)) + _np.roll(cell_data,1,(1,)) + _np.roll(cell_data,1,(2,))
+ _np.roll(cell_data,1,(0,1)) + _np.roll(cell_data,1,(1,2)) + _np.roll(cell_data,1,(2,0)))*0.125
return _np.pad(n,((0,1),(0,1),(0,1))+((0,0),)*len(cell_data.shape[3:]),mode='wrap')
def node_2_point(node_data):
"""Interpolate periodic nodal data to point data."""
c = ( node_data + _np.roll(node_data,1,(0,1,2))
+ _np.roll(node_data,1,(0,)) + _np.roll(node_data,1,(1,)) + _np.roll(node_data,1,(2,))
+ _np.roll(node_data,1,(0,1)) + _np.roll(node_data,1,(1,2)) + _np.roll(node_data,1,(2,0)))*0.125
return c[1:,1:,1:]
def cellsSizeOrigin_coordinates0_node(coordinates0,ordered=True): def cellsSizeOrigin_coordinates0_node(coordinates0,ordered=True):
""" """
Return grid 'DNA', i.e. cells, size, and origin from 1D array of nodal positions. Return grid 'DNA', i.e. cells, size, and origin from 1D array of nodal positions.
@ -402,6 +439,66 @@ def cellsSizeOrigin_coordinates0_node(coordinates0,ordered=True):
return (cells,size,origin) return (cells,size,origin)
def point_to_node(cell_data):
"""
Interpolate periodic point data to nodal data.
Parameters
----------
cell_data : numpy.ndarray of shape (:,:,:,...)
Data defined on the cell centers of a periodic grid.
Returns
-------
node_data : numpy.ndarray of shape (:,:,:,...)
Data defined on the nodes of a periodic grid.
"""
n = ( cell_data + _np.roll(cell_data,1,(0,1,2))
+ _np.roll(cell_data,1,(0,)) + _np.roll(cell_data,1,(1,)) + _np.roll(cell_data,1,(2,))
+ _np.roll(cell_data,1,(0,1)) + _np.roll(cell_data,1,(1,2)) + _np.roll(cell_data,1,(2,0)))*0.125
return _np.pad(n,((0,1),(0,1),(0,1))+((0,0),)*len(cell_data.shape[3:]),mode='wrap')
def node_to_point(node_data):
"""
Interpolate periodic nodal data to point data.
Parameters
----------
node_data : numpy.ndarray of shape (:,:,:,...)
Data defined on the nodes of a periodic grid.
Returns
-------
cell_data : numpy.ndarray of shape (:,:,:,...)
Data defined on the cell centers of a periodic grid.
"""
c = ( node_data + _np.roll(node_data,1,(0,1,2))
+ _np.roll(node_data,1,(0,)) + _np.roll(node_data,1,(1,)) + _np.roll(node_data,1,(2,))
+ _np.roll(node_data,1,(0,1)) + _np.roll(node_data,1,(1,2)) + _np.roll(node_data,1,(2,0)))*0.125
return c[1:,1:,1:]
def coordinates0_valid(coordinates0):
"""
Check whether coordinates lie on a regular grid.
Parameters
----------
coordinates0 : numpy.ndarray
Array of undeformed cell coordinates.
"""
try:
cellsSizeOrigin_coordinates0_point(coordinates0,ordered=True)
return True
except ValueError:
return False
def regrid(size,F,cells): def regrid(size,F,cells):
""" """
Return mapping from coordinates in deformed configuration to a regular grid. Return mapping from coordinates in deformed configuration to a regular grid.

View File

@ -1,4 +1,11 @@
"""Finite-strain continuum mechanics.""" """
Finite-strain continuum mechanics.
Notes
-----
Collection of routines to operate on numpy.ndarrays of shape (...,3,3).
"""
from . import tensor as _tensor from . import tensor as _tensor
from . import _rotation from . import _rotation
@ -154,7 +161,6 @@ def strain(F,t,m):
return eps return eps
def stress_Cauchy(P,F): def stress_Cauchy(P,F):
""" """
Calculate the Cauchy stress (true stress). Calculate the Cauchy stress (true stress).

View File

@ -24,6 +24,11 @@ def from_random(size,N_seeds,cells=None,rng_seed=None):
A seed to initialize the BitGenerator. Defaults to None. A seed to initialize the BitGenerator. Defaults to None.
If None, then fresh, unpredictable entropy will be pulled from the OS. If None, then fresh, unpredictable entropy will be pulled from the OS.
Returns
-------
new : numpy.ndarray of shape (N_seeds,3)
Coordinates in 3D space.
""" """
rng = _np.random.default_rng(rng_seed) rng = _np.random.default_rng(rng_seed)
if cells is None: if cells is None:
@ -56,6 +61,11 @@ def from_Poisson_disc(size,N_seeds,N_candidates,distance,periodic=True,rng_seed=
A seed to initialize the BitGenerator. Defaults to None. A seed to initialize the BitGenerator. Defaults to None.
If None, then fresh, unpredictable entropy will be pulled from the OS. If None, then fresh, unpredictable entropy will be pulled from the OS.
Returns
-------
new : numpy.ndarray of shape (N_seeds,3)
Coordinates in 3D space.
""" """
rng = _np.random.default_rng(rng_seed) rng = _np.random.default_rng(rng_seed)
coords = _np.empty((N_seeds,3)) coords = _np.empty((N_seeds,3))
@ -94,6 +104,11 @@ def from_grid(grid,selection=None,invert=False,average=False,periodic=True):
periodic : boolean, optional periodic : boolean, optional
Center of gravity with periodic boundaries. Center of gravity with periodic boundaries.
Returns
-------
new : numpy.ndarray of shape (...,3)
Coordinates in 3D space.
""" """
material = grid.material.reshape((-1,1),order='F') material = grid.material.reshape((-1,1),order='F')
mask = _np.full(grid.cells.prod(),True,dtype=bool) if selection is None else \ mask = _np.full(grid.cells.prod(),True,dtype=bool) if selection is None else \

View File

@ -60,7 +60,7 @@ class TestGridFilters:
cell_field_x = np.interp(coordinates0_point_x,coordinates_node_x,node_field_x,period=np.pi*2.) cell_field_x = np.interp(coordinates0_point_x,coordinates_node_x,node_field_x,period=np.pi*2.)
cell_field = np.broadcast_to(cell_field_x.reshape(-1,1,1),cells) cell_field = np.broadcast_to(cell_field_x.reshape(-1,1,1),cells)
assert np.allclose(cell_field,grid_filters.node_2_point(node_field)) assert np.allclose(cell_field,grid_filters.node_to_point(node_field))
@pytest.mark.parametrize('mode',['point','node']) @pytest.mark.parametrize('mode',['point','node'])
def test_coordinates0_origin(self,mode): def test_coordinates0_origin(self,mode):
@ -93,14 +93,24 @@ class TestGridFilters:
F = np.broadcast_to(np.random.random((3,3)), tuple(cells)+(3,3)) F = np.broadcast_to(np.random.random((3,3)), tuple(cells)+(3,3))
assert np.allclose(function(size,F),0.0) assert np.allclose(function(size,F),0.0)
@pytest.mark.parametrize('function',[grid_filters.coordinates0_check, @pytest.mark.parametrize('function',[grid_filters.cellsSizeOrigin_coordinates0_point,
grid_filters.cellsSizeOrigin_coordinates0_node, grid_filters.cellsSizeOrigin_coordinates0_node])
grid_filters.cellsSizeOrigin_coordinates0_point])
def test_invalid_coordinates(self,function): def test_invalid_coordinates(self,function):
invalid_coordinates = np.random.random((np.random.randint(12,52),3)) invalid_coordinates = np.random.random((np.random.randint(12,52),3))
with pytest.raises(ValueError): with pytest.raises(ValueError):
function(invalid_coordinates) function(invalid_coordinates)
@pytest.mark.parametrize('function',[grid_filters.coordinates0_point,
grid_filters.coordinates0_node])
def test_valid_coordinates_check(self,function):
valid_coordinates = function(np.random.randint(4,10,(3)),np.random.rand(3))
assert grid_filters.coordinates0_valid(valid_coordinates.reshape(-1,3,order='F'))
def test_invalid_coordinates_check(self):
invalid_coordinates = np.random.random((np.random.randint(12,52),3))
assert not grid_filters.coordinates0_valid(invalid_coordinates)
@pytest.mark.parametrize('function',[grid_filters.cellsSizeOrigin_coordinates0_node, @pytest.mark.parametrize('function',[grid_filters.cellsSizeOrigin_coordinates0_node,
grid_filters.cellsSizeOrigin_coordinates0_point]) grid_filters.cellsSizeOrigin_coordinates0_point])
def test_uneven_spaced_coordinates(self,function): def test_uneven_spaced_coordinates(self,function):