Merge branch 'python-documentation' into 'development'

improvements to python documentation

See merge request damask/DAMASK!371
This commit is contained in:
Philip Eisenlohr 2021-04-25 15:16:04 +00:00
commit b7777bc746
20 changed files with 989 additions and 278 deletions

@ -1 +1 @@
Subproject commit 23eac08c9f9638f8dae76710095222d00f948eec Subproject commit 7f0594060779d9a8a4e774d558134309ab77b96e

View File

@ -5,6 +5,7 @@ references:
10.1016/j.ijplas.2020.102779 10.1016/j.ijplas.2020.102779
- K. Sedighiani et al., - K. Sedighiani et al.,
Mechanics of Materials, submitted Mechanics of Materials, submitted
output: [rho_dip, rho_mob]
N_sl: [12, 12] N_sl: [12, 12]
b_sl: [2.49e-10, 2.49e-10] b_sl: [2.49e-10, 2.49e-10]
rho_mob_0: [2.81e12, 2.8e12] rho_mob_0: [2.81e12, 2.8e12]

View File

@ -1,11 +1,4 @@
""" """Tools for managing DAMASK simulations."""
Tools for pre and post processing of DAMASK simulations.
Modules that contain only one class (of the same name),
are prefixed by a '_'. For example, '_colormap' contains
a class called 'Colormap' which is imported as 'damask.Colormap'.
"""
from pathlib import Path as _Path from pathlib import Path as _Path
import re as _re import re as _re
@ -15,7 +8,6 @@ with open(_Path(__file__).parent/_Path('VERSION')) as _f:
version = _re.sub(r'^v','',_f.readline().strip()) version = _re.sub(r'^v','',_f.readline().strip())
__version__ = version __version__ = version
# make classes directly accessible as damask.Class
from . import util # noqa from . import util # noqa
from . import seeds # noqa from . import seeds # noqa
from . import tensor # noqa from . import tensor # noqa
@ -23,6 +15,8 @@ from . import mechanics # noqa
from . import solver # noqa from . import solver # noqa
from . import grid_filters # noqa from . import grid_filters # noqa
from . import lattice # noqa from . import lattice # noqa
#Modules that contain only one class (of the same name), are prefixed by a '_'.
#For example, '_colormap' containsa class called 'Colormap' which is imported as 'damask.Colormap'.
from ._rotation import Rotation # noqa from ._rotation import Rotation # noqa
from ._orientation import Orientation # noqa from ._orientation import Orientation # noqa
from ._table import Table # noqa from ._table import Table # noqa

View File

@ -91,6 +91,16 @@ class Colormap(mpl.colors.ListedColormap):
- 'lab': CIE Lab. - 'lab': CIE Lab.
- 'msh': Msh (for perceptual uniform interpolation). - 'msh': Msh (for perceptual uniform interpolation).
Returns
-------
new : damask.Colormap
Colormap within given bounds.
Examples
--------
>>> import damask
>>> damask.Colormap.from_range((0,0,1),(0,0,0),'blue_to_black')
""" """
low_high = np.vstack((low,high)) low_high = np.vstack((low,high))
if model.lower() == 'rgb': if model.lower() == 'rgb':
@ -150,6 +160,16 @@ class Colormap(mpl.colors.ListedColormap):
This parameter is not used for matplotlib colormaps This parameter is not used for matplotlib colormaps
that are of type `ListedColormap`. that are of type `ListedColormap`.
Returns
-------
new : damask.Colormap
Predefined colormap.
Examples
--------
>>> import damask
>>> damask.Colormap.from_predefined('strain')
""" """
# matplotlib presets # matplotlib presets
try: try:
@ -220,6 +240,11 @@ class Colormap(mpl.colors.ListedColormap):
damask.Colormap damask.Colormap
The reversed colormap. The reversed colormap.
Examples
--------
>>> import damask
>>> damask.Colormap.from_predefined('stress').reversed()
""" """
rev = super(Colormap,self).reversed(name) rev = super(Colormap,self).reversed(name)
return Colormap(np.array(rev.colors),rev.name[:-4] if rev.name.endswith('_r_r') else rev.name) return Colormap(np.array(rev.colors),rev.name[:-4] if rev.name.endswith('_r_r') else rev.name)
@ -239,8 +264,8 @@ class Colormap(mpl.colors.ListedColormap):
Returns Returns
------- -------
f f : file object
File handle File handle with write access.
""" """
if fname is None: if fname is None:

View File

@ -61,6 +61,11 @@ class Config(dict):
other : damask.Config or dict other : damask.Config or dict
Key-value pairs that update self. Key-value pairs that update self.
Returns
-------
updated : damask.Config
Updated configuration.
""" """
duplicate = self.copy() duplicate = self.copy()
duplicate.update(other) duplicate.update(other)
@ -81,6 +86,11 @@ class Config(dict):
keys : iterable or scalar keys : iterable or scalar
Label of the key(s) to remove. Label of the key(s) to remove.
Returns
-------
updated : damask.Config
Updated configuration.
""" """
duplicate = self.copy() duplicate = self.copy()
for k in keys if isinstance(keys, Iterable) and not isinstance(keys, str) else [keys]: for k in keys if isinstance(keys, Iterable) and not isinstance(keys, str) else [keys]:
@ -98,6 +108,11 @@ class Config(dict):
fname : file, str, or pathlib.Path fname : file, str, or pathlib.Path
Filename or file for writing. Filename or file for writing.
Returns
-------
loaded : damask.Config
Configuration from file.
""" """
try: try:
fhandle = open(fname) fhandle = open(fname)

View File

@ -14,7 +14,7 @@ class ConfigMaterial(Config):
A complete material configuration file has the entries 'material', A complete material configuration file has the entries 'material',
'phase', and 'homogenization'. For use in DAMASK, it needs to be 'phase', and 'homogenization'. For use in DAMASK, it needs to be
stored as 'material.yaml'. stored as 'material.yaml'.
""" """
def __init__(self,d=None): def __init__(self,d=None):
@ -54,60 +54,17 @@ class ConfigMaterial(Config):
Parameters Parameters
---------- ----------
fname : file, str, or pathlib.Path, optional fname : file, str, or pathlib.Path, optional
Filename or file for writing. Defaults to 'material.yaml'. Filename or file to read from. Defaults to 'material.yaml'.
Returns
-------
loaded : damask.ConfigMaterial
Material configuration from file.
""" """
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',
@ -151,6 +108,11 @@ class ConfigMaterial(Config):
and grain- or cell-wise data. Defaults to None, in which case and grain- or cell-wise data. Defaults to None, in which case
it is set as the path that contains _SIMPL_GEOMETRY/SPACING. it is set as the path that contains _SIMPL_GEOMETRY/SPACING.
Returns
-------
loaded : damask.ConfigMaterial
Material configuration from file.
""" """
b = util.DREAM3D_base_group(fname) if base_group is None else base_group b = util.DREAM3D_base_group(fname) if base_group is None else base_group
c = util.DREAM3D_cell_data_group(fname) if cell_data is None else cell_data c = util.DREAM3D_cell_data_group(fname) if cell_data is None else cell_data
@ -181,9 +143,74 @@ 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.
Returns
-------
new : damask.ConfigMaterial
Material configuration from values in 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 +263,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 +321,7 @@ class ConfigMaterial(Config):
Returns Returns
------- -------
cfg : damask.ConfigMaterial updated : damask.ConfigMaterial
Updated material configuration. Updated material configuration.
""" """
@ -311,7 +350,7 @@ class ConfigMaterial(Config):
Returns Returns
------- -------
cfg : damask.ConfigMaterial updated : damask.ConfigMaterial
Updated material configuration. Updated material configuration.
""" """
@ -336,53 +375,61 @@ class ConfigMaterial(Config):
Returns Returns
------- -------
cfg : damask.ConfigMaterial updated : damask.ConfigMaterial
Updated material configuration. Updated material configuration.
Examples Examples
-------- --------
Create a dual-phase steel microstructure for micromechanical simulations:
>>> import numpy as np >>> import numpy as np
>>> import damask >>> import damask
>>> m = damask.ConfigMaterial().material_add(phase = ['Aluminum','Steel'], >>> m = damask.ConfigMaterial()
... O = damask.Rotation.from_random(2), >>> m = m.material_add(phase = ['Ferrite','Martensite'],
... homogenization = 'SX') ... O = damask.Rotation.from_random(2),
... homogenization = 'SX')
>>> m >>> m
material: material:
- constituents: - constituents:
- O: [0.577764, -0.146299, -0.617669, 0.513010] - O: [0.577764, -0.146299, -0.617669, 0.513010]
v: 1.0 v: 1.0
phase: Aluminum phase: Ferrite
homogenization: SX homogenization: SX
- constituents: - constituents:
- O: [0.184176, 0.340305, 0.737247, 0.553840] - O: [0.184176, 0.340305, 0.737247, 0.553840]
v: 1.0 v: 1.0
phase: Steel phase: Martensite
homogenization: SX homogenization: SX
homogenization: {} homogenization: {}
phase: {} phase: {}
>>> m = damask.ConfigMaterial().material_add(phase = np.array(['Austenite','Martensite']).reshape(1,2), Create a duplex stainless steel microstructure for forming simulations:
... O = damask.Rotation.from_random((2,2)),
... v = np.array([0.2,0.8]).reshape(1,2), >>> import numpy as np
... homogenization = ['A','B']) >>> import damask
>>> m = damask.ConfigMaterial()
>>> m = m.material_add(phase = np.array(['Austenite','Ferrite']).reshape(1,2),
... O = damask.Rotation.from_random((2,2)),
... v = np.array([0.2,0.8]).reshape(1,2),
... homogenization = 'Taylor')
>>> m >>> m
material: material:
- constituents: - constituents:
- phase: Austenite - phase: Austenite
O: [0.659802978293224, 0.6953785848195171, 0.22426295326327111, -0.17554139512785227] O: [0.659802978293224, 0.6953785848195171, 0.22426295326327111, -0.17554139512785227]
v: 0.2 v: 0.2
- phase: Martensite - phase: Ferrite
O: [0.49356745891301596, 0.2841806579193434, -0.7487679215072818, -0.339085707289975] O: [0.49356745891301596, 0.2841806579193434, -0.7487679215072818, -0.339085707289975]
v: 0.8 v: 0.8
homogenization: A homogenization: Taylor
- constituents: - constituents:
- phase: Austenite - phase: Austenite
O: [0.26542221365204055, 0.7268854930702071, 0.4474726435701472, -0.44828201137283735] O: [0.26542221365204055, 0.7268854930702071, 0.4474726435701472, -0.44828201137283735]
v: 0.2 v: 0.2
- phase: Martensite - phase: Ferrite
O: [0.6545817158479885, -0.08004812803625233, -0.6226561293931374, 0.4212059104577611] O: [0.6545817158479885, -0.08004812803625233, -0.6226561293931374, 0.4212059104577611]
v: 0.8 v: 0.8
homogenization: B homogenization: Taylor
homogenization: {} homogenization: {}
phase: {} phase: {}

View File

@ -161,6 +161,11 @@ class Grid:
Grid file to read. Valid extension is .vtr, which will be appended Grid file to read. Valid extension is .vtr, which will be appended
if not given. if not given.
Returns
-------
loaded : damask.Grid
Grid-based geometry from file.
""" """
v = VTK.load(fname if str(fname).endswith('.vtr') else str(fname)+'.vtr') v = VTK.load(fname if str(fname).endswith('.vtr') else str(fname)+'.vtr')
comments = v.get_comments() comments = v.get_comments()
@ -190,6 +195,11 @@ class Grid:
fname : str, pathlib.Path, or file handle fname : str, pathlib.Path, or file handle
Geometry file to read. Geometry file to read.
Returns
-------
loaded : damask.Grid
Grid-based geometry from file.
""" """
warnings.warn('Support for ASCII-based geom format will be removed in DAMASK 3.1.0', DeprecationWarning,2) warnings.warn('Support for ASCII-based geom format will be removed in DAMASK 3.1.0', DeprecationWarning,2)
try: try:
@ -281,6 +291,10 @@ class Grid:
and grain- or cell-wise data. Defaults to None, in which case and grain- or cell-wise data. Defaults to None, in which case
it is set as the path that contains _SIMPL_GEOMETRY/SPACING. it is set as the path that contains _SIMPL_GEOMETRY/SPACING.
Returns
-------
loaded : damask.Grid
Grid-based geometry from file.
""" """
b = util.DREAM3D_base_group(fname) if base_group is None else base_group b = util.DREAM3D_base_group(fname) if base_group is None else base_group
@ -319,6 +333,11 @@ class Grid:
Label(s) of the columns containing the material definition. Label(s) of the columns containing the material definition.
Each unique combination of values results in one material ID. Each unique combination of values results in one material ID.
Returns
-------
new : damask.Grid
Grid-based geometry from values in table.
""" """
cells,size,origin = grid_filters.cellsSizeOrigin_coordinates0_point(table.get(coordinates)) cells,size,origin = grid_filters.cellsSizeOrigin_coordinates0_point(table.get(coordinates))
@ -356,6 +375,11 @@ class Grid:
periodic : Boolean, optional periodic : Boolean, optional
Assume grid to be periodic. Defaults to True. Assume grid to be periodic. Defaults to True.
Returns
-------
new : damask.Grid
Grid-based geometry from tessellation.
""" """
if periodic: if periodic:
weights_p = np.tile(weights,27) # Laguerre weights (1,2,3,1,2,3,...,1,2,3) weights_p = np.tile(weights,27) # Laguerre weights (1,2,3,1,2,3,...,1,2,3)
@ -405,6 +429,11 @@ class Grid:
periodic : Boolean, optional periodic : Boolean, optional
Assume grid to be periodic. Defaults to True. Assume grid to be periodic. Defaults to True.
Returns
-------
new : damask.Grid
Grid-based geometry from tessellation.
""" """
coords = grid_filters.coordinates0_point(cells,size).reshape(-1,3) coords = grid_filters.coordinates0_point(cells,size).reshape(-1,3)
KDTree = spatial.cKDTree(seeds,boxsize=size) if periodic else spatial.cKDTree(seeds) KDTree = spatial.cKDTree(seeds,boxsize=size) if periodic else spatial.cKDTree(seeds)
@ -478,6 +507,11 @@ class Grid:
materials : (int, int), optional materials : (int, int), optional
Material IDs. Defaults to (1,2). Material IDs. Defaults to (1,2).
Returns
-------
new : damask.Grid
Grid-based geometry from definition of minimal surface.
Notes Notes
----- -----
The following triply-periodic minimal surfaces are implemented: The following triply-periodic minimal surfaces are implemented:
@ -598,6 +632,11 @@ class Grid:
periodic : Boolean, optional periodic : Boolean, optional
Assume grid to be periodic. Defaults to True. Assume grid to be periodic. Defaults to True.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
# radius and center # radius and center
r = np.array(dimension)/2.0*self.size/self.cells if np.array(dimension).dtype in np.sctypes['int'] else \ r = np.array(dimension)/2.0*self.size/self.cells if np.array(dimension).dtype in np.sctypes['int'] else \
@ -638,6 +677,11 @@ class Grid:
reflect : bool, optional reflect : bool, optional
Reflect (include) outermost layers. Defaults to False. Reflect (include) outermost layers. Defaults to False.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
valid = ['x','y','z'] valid = ['x','y','z']
if not set(directions).issubset(valid): if not set(directions).issubset(valid):
@ -670,6 +714,11 @@ class Grid:
Direction(s) along which the grid is flipped. Direction(s) along which the grid is flipped.
Valid entries are 'x', 'y', 'z'. Valid entries are 'x', 'y', 'z'.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
valid = ['x','y','z'] valid = ['x','y','z']
if not set(directions).issubset(valid): if not set(directions).issubset(valid):
@ -695,6 +744,11 @@ class Grid:
periodic : Boolean, optional periodic : Boolean, optional
Assume grid to be periodic. Defaults to True. Assume grid to be periodic. Defaults to True.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
return Grid(material = ndimage.interpolation.zoom( return Grid(material = ndimage.interpolation.zoom(
self.material, self.material,
@ -723,6 +777,11 @@ class Grid:
periodic : Boolean, optional periodic : Boolean, optional
Assume grid to be periodic. Defaults to True. Assume grid to be periodic. Defaults to True.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
def mostFrequent(arr,selection=None): def mostFrequent(arr,selection=None):
me = arr[arr.size//2] me = arr[arr.size//2]
@ -746,7 +805,15 @@ class Grid:
def renumber(self): def renumber(self):
"""Renumber sorted material indices as 0,...,N-1.""" """
Renumber sorted material indices as 0,...,N-1.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
"""
_,renumbered = np.unique(self.material,return_inverse=True) _,renumbered = np.unique(self.material,return_inverse=True)
return Grid(material = renumbered.reshape(self.cells), return Grid(material = renumbered.reshape(self.cells),
@ -767,6 +834,11 @@ class Grid:
fill : int or float, optional fill : int or float, optional
Material index to fill the corners. Defaults to material.max() + 1. Material index to fill the corners. Defaults to material.max() + 1.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
if fill is None: fill = np.nanmax(self.material) + 1 if fill is None: fill = np.nanmax(self.material) + 1
dtype = float if isinstance(fill,float) or self.material.dtype in np.sctypes['float'] else int dtype = float if isinstance(fill,float) or self.material.dtype in np.sctypes['float'] else int
@ -802,6 +874,11 @@ class Grid:
fill : int or float, optional fill : int or float, optional
Material index to fill the background. Defaults to material.max() + 1. Material index to fill the background. Defaults to material.max() + 1.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
if offset is None: offset = 0 if offset is None: offset = 0
if fill is None: fill = np.nanmax(self.material) + 1 if fill is None: fill = np.nanmax(self.material) + 1
@ -834,6 +911,11 @@ class Grid:
to_material : iterable of ints to_material : iterable of ints
New material indices. New material indices.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
def mp(entry,mapper): def mp(entry,mapper):
return mapper[entry] if entry in mapper else entry return mapper[entry] if entry in mapper else entry
@ -849,7 +931,15 @@ class Grid:
def sort(self): def sort(self):
"""Sort material indices such that min(material) is located at (0,0,0).""" """
Sort material indices such that min(material) is located at (0,0,0).
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
"""
a = self.material.flatten(order='F') a = self.material.flatten(order='F')
from_ma = pd.unique(a) from_ma = pd.unique(a)
sort_idx = np.argsort(from_ma) sort_idx = np.argsort(from_ma)
@ -884,6 +974,11 @@ class Grid:
periodic : Boolean, optional periodic : Boolean, optional
Assume grid to be periodic. Defaults to True. Assume grid to be periodic. Defaults to True.
Returns
-------
updated : damask.Grid
Updated grid-based geometry.
""" """
def tainted_neighborhood(stencil,trigger): def tainted_neighborhood(stencil,trigger):
@ -917,6 +1012,11 @@ class Grid:
Direction(s) along which the boundaries are determined. Direction(s) along which the boundaries are determined.
Valid entries are 'x', 'y', 'z'. Defaults to 'xyz'. Valid entries are 'x', 'y', 'z'. Defaults to 'xyz'.
Returns
-------
grain_boundaries : damask.VTK
VTK-based geometry of grain boundary network.
""" """
valid = ['x','y','z'] valid = ['x','y','z']
if not set(directions).issubset(valid): if not set(directions).issubset(valid):

View File

@ -8,15 +8,15 @@ from . import tensor
_parameter_doc = \ _parameter_doc = \
"""lattice : str """lattice : str
Either a crystal family out of [triclinic, monoclinic, orthorhombic, tetragonal, hexagonal, cubic] Either a crystal family out of {triclinic, monoclinic, orthorhombic, tetragonal, hexagonal, cubic}
or a Bravais lattice out of [aP, mP, mS, oP, oS, oI, oF, tP, tI, hP, cP, cI, cF]. or a Bravais lattice out of {aP, mP, mS, oP, oS, oI, oF, tP, tI, hP, cP, cI, cF}.
When specifying a Bravais lattice, additional lattice parameters might be required: When specifying a Bravais lattice, additional lattice parameters might be required.
a : float, optional a : float, optional
Length of lattice parameter "a". Length of lattice parameter 'a'.
b : float, optional b : float, optional
Length of lattice parameter "b". Length of lattice parameter 'b'.
c : float, optional c : float, optional
Length of lattice parameter "c". Length of lattice parameter 'c'.
alpha : float, optional alpha : float, optional
Angle between b and c lattice basis. Angle between b and c lattice basis.
beta : float, optional beta : float, optional

View File

@ -34,7 +34,7 @@ def _read(dataset):
return np.array(dataset,dtype=dtype) return np.array(dataset,dtype=dtype)
def _match(requested,existing): def _match(requested,existing):
"""Find matches among two sets of labels.""" """Find matches among two sets of names."""
def flatten_list(list_of_lists): def flatten_list(list_of_lists):
return [e for e_ in list_of_lists for e in e_] return [e for e_ in list_of_lists for e in e_]
@ -57,11 +57,30 @@ def _empty_like(dataset,N_materialpoints,fill_float,fill_int):
class Result: class Result:
""" """
Manipulate and read DADF5 files. Add data to and export data from a DADF5 file.
A DADF5 (DAMASK HDF5) file contains DAMASK results.
Its group/folder structure reflects the layout in material.yaml.
This class provides a customizable view on the DADF5 file.
Upon initialization, all attributes are visible.
Derived quantities are added to the file and existing data is
exported based on the current view.
Examples
--------
Open 'my_file.hdf5', which is assumed to contain deformation gradient 'F'
and first Piola-Kirchhoff stress 'P', add the Mises equivalent of the
Cauchy stress, and export it to VTK (file) and numpy.ndarray (memory).
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_Cauchy()
>>> r.add_equivalent_Mises('sigma')
>>> r.save_VTK()
>>> r_last = r.view('increments',-1)
>>> sigma_vM_last = r_last.get('sigma_vM')
DADF5 (DAMASK HDF5) files contain DAMASK results.
The group/folder structure reflects the input data
in material.yaml.
""" """
def __init__(self,fname): def __init__(self,fname):
@ -163,6 +182,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
Modified or new view on the DADF5 file.
""" """
# allow True/False and string arguments # allow True/False and string arguments
if datasets is True: if datasets is True:
@ -176,6 +200,7 @@ class Result:
if what == 'increments': if what == 'increments':
choice = [c if isinstance(c,str) and c.startswith(inc) else choice = [c if isinstance(c,str) and c.startswith(inc) else
f'{inc}{c}' for c in choice] f'{inc}{c}' for c in choice]
if datasets == -1: choice = [self.increments[-1]]
elif what == 'times': elif what == 'times':
what = 'increments' what = 'increments'
if choice == ['*']: if choice == ['*']:
@ -207,15 +232,31 @@ class Result:
return dup return dup
def allow_modification(self): def modification_enable(self):
"""Allow to overwrite existing data.""" """
Allow modification of existing data.
Returns
-------
modified_view : damask.Result
View without write-protection of existing data.
"""
print(util.warn('Warning: Modification of existing datasets allowed!')) print(util.warn('Warning: Modification of existing datasets allowed!'))
dup = self.copy() dup = self.copy()
dup._allow_modification = True dup._allow_modification = True
return dup return dup
def disallow_modification(self): def modification_disable(self):
"""Disallow to overwrite existing data (default case).""" """
Prevent modification of existing data (default case).
Returns
-------
modified_view : damask.Result
View with write-protection of existing data.
"""
dup = self.copy() dup = self.copy()
dup._allow_modification = False dup._allow_modification = False
return dup return dup
@ -223,7 +264,7 @@ class Result:
def increments_in_range(self,start,end): def increments_in_range(self,start,end):
""" """
Select all increments within a given range. Get all increments within a given range.
Parameters Parameters
---------- ----------
@ -232,6 +273,11 @@ class Result:
end : int or str end : int or str
End increment. End increment.
Returns
-------
increments : list of ints
Increment number of all increments within the given bounds.
""" """
# compatibility hack # compatibility hack
ln = 3 if self.version_minor < 12 else 10 ln = 3 if self.version_minor < 12 else 10
@ -239,13 +285,13 @@ class Result:
for i,inc in enumerate([int(i[ln:]) for i in self.increments]): for i,inc in enumerate([int(i[ln:]) for i in self.increments]):
s,e = map(lambda x: int(x[ln:] if isinstance(x,str) and x.startswith('inc') else x), (start,end)) s,e = map(lambda x: int(x[ln:] if isinstance(x,str) and x.startswith('inc') else x), (start,end))
if s <= inc <= e: if s <= inc <= e:
selected.append(self.increments[i]) selected.append(int(self.increments[i].split('_')[1]))
return selected return selected
def times_in_range(self,start,end): def times_in_range(self,start,end):
""" """
Select all increments within a given time range. Get all increments within a given time range.
Parameters Parameters
---------- ----------
@ -254,6 +300,11 @@ class Result:
end : float end : float
Time of end increment. Time of end increment.
Returns
-------
times : list of float
Simulation time of all increments within the given bounds.
""" """
selected = [] selected = []
for i,time in enumerate(self.times): for i,time in enumerate(self.times):
@ -274,6 +325,25 @@ 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 only the selected attributes being visible.
Examples
--------
Get a view that shows only results from the initial configuration:
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r_first = r.view('increment',0)
Get a view that shows all results between simulation times of 10 to 40:
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r_t10to40 = r.view('times',r.times_in_range(10.0,40.0))
""" """
return self._manage_view('set',what,datasets) return self._manage_view('set',what,datasets)
@ -290,13 +360,27 @@ 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 additional visible attributes.
Examples
--------
Get a view that shows only results from first and last increment:
>>> import damask
>>> r_empty = damask.Result('my_file.hdf5').view('increments',False)
>>> r_first = r_empty.view_more('increments',0)
>>> r_first_and_last = r.first.view_more('increments',-1)
""" """
return self._manage_view('add',what,datasets) return self._manage_view('add',what,datasets)
def view_less(self,what,datasets): def view_less(self,what,datasets):
""" """
Delete from view. Remove from view.
Parameters Parameters
---------- ----------
@ -306,37 +390,98 @@ 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 fewer visible attributes.
Examples
--------
Get a view that omits the undeformed configuration:
>>> import damask
>>> r_all = damask.Result('my_file.hdf5')
>>> r_deformed = r_all.view_less('increments',0)
""" """
return self._manage_view('del',what,datasets) return self._manage_view('del',what,datasets)
def rename(self,name_old,name_new): def rename(self,name_src,name_dst):
""" """
Rename dataset. Rename/move datasets (within the same group/folder).
This operation is discouraged because the history of the
data becomes untracable and scientific integrity cannot be
ensured.
Parameters Parameters
---------- ----------
name_old : str name_src : str
Name of the dataset to be renamed. Name of the datasets to be renamed.
name_new : str name_dst : str
New name of the dataset. New name of the datasets.
Examples
--------
Rename datasets containing the deformation gradient from 'F' to 'def_grad':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r_unprotected = r.modification_enable()
>>> r_unprotected.rename('F','def_grad')
""" """
if not self._allow_modification: if not self._allow_modification:
raise PermissionError('Rename operation not permitted') raise PermissionError('Renaming datasets not permitted')
with h5py.File(self.fname,'a') as f: with h5py.File(self.fname,'a') as f:
for inc in self.visible['increments']: for inc in self.visible['increments']:
for ty in ['phase','homogenization']: for ty in ['phase','homogenization']:
for label in self.visible[ty+'s']: for label in self.visible[ty+'s']:
for field in _match(self.visible['fields'],f['/'.join([inc,ty,label])].keys()): for field in _match(self.visible['fields'],f['/'.join([inc,ty,label])].keys()):
path_old = '/'.join([inc,ty,label,field,name_old]) path_src = '/'.join([inc,ty,label,field,name_src])
path_new = '/'.join([inc,ty,label,field,name_new]) path_dst = '/'.join([inc,ty,label,field,name_dst])
if path_old in f.keys(): if path_src in f.keys():
f[path_new] = f[path_old] f[path_dst] = f[path_src]
f[path_new].attrs['renamed'] = f'original name: {name_old}' if h5py3 else \ f[path_dst].attrs['renamed'] = f'original name: {name_src}' if h5py3 else \
f'original name: {name_old}'.encode() f'original name: {name_src}'.encode()
del f[path_old] del f[path_src]
def remove(self,name):
"""
Remove/delete datasets.
This operation is discouraged because the history of the
data becomes untracable and scientific integrity cannot be
ensured.
Parameters
----------
name : str
Name of the datasets to be deleted.
Examples
--------
Delete the deformation gradient 'F':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r_unprotected = r.modification_enable()
>>> r_unprotected.remove('F')
"""
if not self._allow_modification:
raise PermissionError('Removing datasets not permitted')
with h5py.File(self.fname,'a') as f:
for inc in self.visible['increments']:
for ty in ['phase','homogenization']:
for label in self.visible[ty+'s']:
for field in _match(self.visible['fields'],f['/'.join([inc,ty,label])].keys()):
path = '/'.join([inc,ty,label,field,name])
if path in f.keys(): del f[path]
def list_data(self): def list_data(self):
@ -372,7 +517,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 +526,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 +535,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:
@ -418,7 +564,7 @@ class Result:
Parameters Parameters
---------- ----------
x : str x : str
Label of scalar, vector, or tensor dataset to take absolute value of. Name of scalar, vector, or tensor dataset to take absolute value of.
""" """
self._add_generic_pointwise(self._add_absolute,{'x':x}) self._add_generic_pointwise(self._add_absolute,{'x':x})
@ -439,24 +585,52 @@ class Result:
'creator': 'add_calculation' 'creator': 'add_calculation'
} }
} }
def add_calculation(self,label,formula,unit='n/a',description=None): def add_calculation(self,formula,name,unit='n/a',description=None):
""" """
Add result of a general formula. Add result of a general formula.
Parameters Parameters
---------- ----------
label : str
Label of resulting dataset.
formula : str formula : str
Formula to calculate resulting dataset. Existing datasets are referenced by '#TheirLabel#'. Formula to calculate resulting dataset.
Existing datasets are referenced by '#TheirName#'.
name : str
Name of resulting dataset.
unit : str, optional unit : str, optional
Physical unit of the result. Physical unit of the result.
description : str, optional description : str, optional
Human-readable description of the result. Human-readable description of the result.
Examples
--------
Add total dislocation density, i.e. the sum of mobile dislocation
density 'rho_mob' and dislocation dipole density 'rho_dip' over
all slip systems:
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_calculation('np.sum(#rho_mob#,axis=1)','rho_mob_total',
... '1/m²','total mobile dislocation density')
>>> r.add_calculation(''np.sum(#rho_dip#,axis=1)',rho_dip_total',
... '1/m²','total dislocation dipole density')
>>> r.add_calculation('#rho_dip_total#+#rho_mob_total','rho_total',
... '1/m²','total dislocation density')
Add Mises equivalent of the Cauchy stress without storage of
intermediate results. Define a user function for better readability:
>>> import damask
>>> def equivalent_stress(F,P):
... sigma = damask.mechanics.stress_Cauchy(F=F,P=P)
... return damask.mechanics.equivalent_stress_Mises(sigma)
>>> r = damask.Result('my_file.hdf5')
>>> r.enable_user_function(equivalent_stress)
>>> r.add_calculation('equivalent_stress(#F#,#P#)','sigma_vM','Pa',
... 'Mises equivalent of the Cauchy stress')
""" """
dataset_mapping = {d:d for d in set(re.findall(r'#(.*?)#',formula))} # datasets used in the formula dataset_mapping = {d:d for d in set(re.findall(r'#(.*?)#',formula))} # datasets used in the formula
args = {'formula':formula,'label':label,'unit':unit,'description':description} args = {'formula':formula,'label':name,'unit':unit,'description':description}
self._add_generic_pointwise(self._add_calculation,dataset_mapping,args) self._add_generic_pointwise(self._add_calculation,dataset_mapping,args)
@ -480,9 +654,9 @@ class Result:
Parameters Parameters
---------- ----------
P : str, optional P : str, optional
Label of the dataset containing the first Piola-Kirchhoff stress. Defaults to 'P'. Name of the dataset containing the first Piola-Kirchhoff stress. Defaults to 'P'.
F : str, optional F : str, optional
Label of the dataset containing the deformation gradient. Defaults to 'F'. Name of the dataset containing the deformation gradient. Defaults to 'F'.
""" """
self._add_generic_pointwise(self._add_stress_Cauchy,{'P':P,'F':F}) self._add_generic_pointwise(self._add_stress_Cauchy,{'P':P,'F':F})
@ -506,7 +680,15 @@ class Result:
Parameters Parameters
---------- ----------
T : str T : str
Label of tensor dataset. Name of tensor dataset.
Examples
--------
Add the determinant of plastic deformation gradient 'F_p':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_determinant('F_p')
""" """
self._add_generic_pointwise(self._add_determinant,{'T':T}) self._add_generic_pointwise(self._add_determinant,{'T':T})
@ -530,7 +712,7 @@ class Result:
Parameters Parameters
---------- ----------
T : str T : str
Label of tensor dataset. Name of tensor dataset.
""" """
self._add_generic_pointwise(self._add_deviator,{'T':T}) self._add_generic_pointwise(self._add_deviator,{'T':T})
@ -561,7 +743,7 @@ class Result:
Parameters Parameters
---------- ----------
T_sym : str T_sym : str
Label of symmetric tensor dataset. Name of symmetric tensor dataset.
eigenvalue : str, optional eigenvalue : str, optional
Eigenvalue. Select from 'max', 'mid', 'min'. Defaults to 'max'. Eigenvalue. Select from 'max', 'mid', 'min'. Defaults to 'max'.
@ -594,7 +776,7 @@ class Result:
Parameters Parameters
---------- ----------
T_sym : str T_sym : str
Label of symmetric tensor dataset. Name of symmetric tensor dataset.
eigenvalue : str, optional eigenvalue : str, optional
Eigenvalue to which the eigenvector corresponds. Eigenvalue to which the eigenvector corresponds.
Select from 'max', 'mid', 'min'. Defaults to 'max'. Select from 'max', 'mid', 'min'. Defaults to 'max'.
@ -634,9 +816,17 @@ class Result:
l : numpy.array of shape (3) l : numpy.array of shape (3)
Lab frame direction for inverse pole figure. Lab frame direction for inverse pole figure.
q : str q : str
Label of the dataset containing the crystallographic orientation as quaternions. Name of the dataset containing the crystallographic orientation as quaternions.
Defaults to 'O'. Defaults to 'O'.
Examples
--------
Add the IPF color along [0,1,1] for orientation 'O':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_IPF_color(np.array([0,1,1]))
""" """
self._add_generic_pointwise(self._add_IPF_color,{'q':q},{'l':l}) self._add_generic_pointwise(self._add_IPF_color,{'q':q},{'l':l})
@ -659,7 +849,7 @@ class Result:
Parameters Parameters
---------- ----------
T_sym : str T_sym : str
Label of symmetric tensor dataset. Name of symmetric tensor dataset.
""" """
self._add_generic_pointwise(self._add_maximum_shear,{'T_sym':T_sym}) self._add_generic_pointwise(self._add_maximum_shear,{'T_sym':T_sym})
@ -693,11 +883,25 @@ class Result:
Parameters Parameters
---------- ----------
T_sym : str T_sym : str
Label of symmetric tensorial stress or strain dataset. Name of symmetric tensorial stress or strain dataset.
kind : {'stress', 'strain', None}, optional kind : {'stress', 'strain', None}, optional
Kind of the von Mises equivalent. Defaults to None, in which case Kind of the von Mises equivalent. Defaults to None, in which case
it is selected based on the unit of the dataset ('1' -> strain, 'Pa' -> stress). it is selected based on the unit of the dataset ('1' -> strain, 'Pa' -> stress).
Examples
--------
Add the Mises equivalent of the Cauchy stress 'sigma':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_equivalent_Mises('sigma')
Add the Mises equivalent of the spatial logarithmic strain 'epsilon_V^0.0(F)':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_equivalent_Mises('epsilon_V^0.0(F)')
""" """
self._add_generic_pointwise(self._add_equivalent_Mises,{'T_sym':T_sym},{'kind':kind}) self._add_generic_pointwise(self._add_equivalent_Mises,{'T_sym':T_sym},{'kind':kind})
@ -732,7 +936,7 @@ class Result:
Parameters Parameters
---------- ----------
x : str x : str
Label of vector or tensor dataset. Name of vector or tensor dataset.
ord : {non-zero int, inf, -inf, 'fro', 'nuc'}, optional ord : {non-zero int, inf, -inf, 'fro', 'nuc'}, optional
Order of the norm. inf means NumPys inf object. For details refer to numpy.linalg.norm. Order of the norm. inf means NumPys inf object. For details refer to numpy.linalg.norm.
@ -760,9 +964,9 @@ class Result:
Parameters Parameters
---------- ----------
P : str, optional P : str, optional
Label of first Piola-Kirchhoff stress dataset. Defaults to 'P'. Name of first Piola-Kirchhoff stress dataset. Defaults to 'P'.
F : str, optional F : str, optional
Label of deformation gradient dataset. Defaults to 'F'. Name of deformation gradient dataset. Defaults to 'F'.
""" """
self._add_generic_pointwise(self._add_stress_second_Piola_Kirchhoff,{'P':P,'F':F}) self._add_generic_pointwise(self._add_stress_second_Piola_Kirchhoff,{'P':P,'F':F})
@ -801,7 +1005,7 @@ class Result:
# Parameters # Parameters
# ---------- # ----------
# q : str # q : str
# Label of the dataset containing the crystallographic orientation as quaternions. # Name of the dataset containing the crystallographic orientation as quaternions.
# p : numpy.array of shape (3) # p : numpy.array of shape (3)
# Crystallographic direction or plane. # Crystallographic direction or plane.
# polar : bool, optional # polar : bool, optional
@ -828,8 +1032,16 @@ class Result:
Parameters Parameters
---------- ----------
F : str, optional F : str
Label of deformation gradient dataset. Name of deformation gradient dataset.
Examples
--------
Add the rotational part of deformation gradient 'F':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_rotation('F')
""" """
self._add_generic_pointwise(self._add_rotation,{'F':F}) self._add_generic_pointwise(self._add_rotation,{'F':F})
@ -853,7 +1065,15 @@ class Result:
Parameters Parameters
---------- ----------
T : str T : str
Label of tensor dataset. Name of tensor dataset.
Examples
--------
Add the hydrostatic part of the Cauchy stress 'sigma':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.add_spherical('sigma')
""" """
self._add_generic_pointwise(self._add_spherical,{'T':T}) self._add_generic_pointwise(self._add_spherical,{'T':T})
@ -879,13 +1099,28 @@ class Result:
Parameters Parameters
---------- ----------
F : str, optional F : str, optional
Label of deformation gradient dataset. Defaults to 'F'. Name of deformation gradient dataset. Defaults to 'F'.
t : {'V', 'U'}, optional t : {'V', 'U'}, optional
Type of the polar decomposition, 'V' for left stretch tensor and 'U' for right stretch tensor. Type of the polar decomposition, 'V' for left stretch tensor and 'U' for right stretch tensor.
Defaults to 'V'. Defaults to 'V'.
m : float, optional m : float, optional
Order of the strain calculation. Defaults to 0.0. Order of the strain calculation. Defaults to 0.0.
Examples
--------
Add the Biot strain based on the deformation gradient 'F':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.strain(t='U',m=0.5)
Add the plastic Euler-Almansi strain based on the
plastic deformation gradient 'F_p':
>>> import damask
>>> r = damask.Result('my_file.hdf5')
>>> r.strain('F_p','V',-1)
""" """
self._add_generic_pointwise(self._add_strain,{'F':F},{'t':t,'m':m}) self._add_generic_pointwise(self._add_strain,{'F':F},{'t':t,'m':m})
@ -909,7 +1144,7 @@ class Result:
Parameters Parameters
---------- ----------
F : str, optional F : str, optional
Label of deformation gradient dataset. Defaults to 'F'. Name of deformation gradient dataset. Defaults to 'F'.
t : {'V', 'U'}, optional t : {'V', 'U'}, optional
Type of the polar decomposition, 'V' for left stretch tensor and 'U' for right stretch tensor. Type of the polar decomposition, 'V' for left stretch tensor and 'U' for right stretch tensor.
Defaults to 'V'. Defaults to 'V'.
@ -1016,10 +1251,14 @@ class Result:
""" """
Write XDMF file to directly visualize data in DADF5 file. Write XDMF file to directly visualize data in DADF5 file.
The XDMF format is only supported for structured grids
with single phase and single constituent.
For other cases use `save_VTK`.
Parameters Parameters
---------- ----------
output : (list of) str output : (list of) str
Labels of the datasets to read. Names of the datasets included in the XDMF file.
Defaults to '*', in which case all datasets are considered. Defaults to '*', in which case all datasets are considered.
""" """
@ -1149,10 +1388,16 @@ class Result:
""" """
Export to VTK cell/point data. Export to VTK cell/point data.
One VTK file per visible increment is created.
For cell data, the VTK format is a rectilinear grid (.vtr) for
grid-based simulations and an unstructured grid (.vtu) for
mesh-baed simulations. For point data, the VTK format is poly
data (.vtp).
Parameters Parameters
---------- ----------
output : (list of) str, optional output : (list of) str, optional
Labels of the datasets to place. Names of the datasets to export to the VTK file.
Defaults to '*', in which case all datasets are exported. Defaults to '*', in which case all datasets are exported.
mode : {'cell', 'point'} mode : {'cell', 'point'}
Export in cell format or point format. Export in cell format or point format.
@ -1231,7 +1476,7 @@ class Result:
Parameters Parameters
---------- ----------
output : (list of) str output : (list of) str
Labels of the datasets to read. Names of the datasets to read.
Defaults to '*', in which case all datasets are read. Defaults to '*', in which case all datasets are read.
flatten : bool flatten : bool
Remove singular levels of the folder hierarchy. Remove singular levels of the folder hierarchy.
@ -1277,13 +1522,13 @@ class Result:
in the DADF5 file. in the DADF5 file.
Multi-phase data is fused into a single output. Multi-phase data is fused into a single output.
`place` is equivalent to `read` if only one phase/homogenization `place` is equivalent to `get` if only one phase/homogenization
and one constituent is present. and one constituent is present.
Parameters Parameters
---------- ----------
output : (list of) str, optional output : (list of) str, optional
Labels of the datasets to place. Names of the datasets to read.
Defaults to '*', in which case all datasets are placed. Defaults to '*', in which case all datasets are placed.
flatten : bool flatten : bool
Remove singular levels of the folder hierarchy. Remove singular levels of the folder hierarchy.
@ -1293,7 +1538,7 @@ class Result:
Remove branches with no data. Defaults to True. Remove branches with no data. Defaults to True.
constituents : (list of) int, optional constituents : (list of) int, optional
Constituents to consider. Constituents to consider.
Defaults to 'None', in which case all constituents are considered. Defaults to None, in which case all constituents are considered.
fill_float : float fill_float : float
Fill value for non-existent entries of floating point type. Fill value for non-existent entries of floating point type.
Defaults to NaN. Defaults to NaN.

View File

@ -24,7 +24,7 @@ class Rotation:
- Euler angle triplets are implemented using the Bunge convention, - Euler angle triplets are implemented using the Bunge convention,
with angular ranges of [0,2π], [0,π], [0,2π]. with angular ranges of [0,2π], [0,π], [0,2π].
- The rotation angle ω is limited to the interval [0,π]. - The rotation angle ω is limited to the interval [0,π].
- The real part of a quaternion is positive, Re(q) > 0 - The real part of a quaternion is positive, Re(q) 0
- P = -1 (as default). - P = -1 (as default).
Examples Examples
@ -357,7 +357,7 @@ class Rotation:
Parameters Parameters
---------- ----------
other : Rotation or list of Rotations. other : damask.Rotation
""" """
return self.copy(rotation=np.vstack(tuple(map(lambda x:x.quaternion, return self.copy(rotation=np.vstack(tuple(map(lambda x:x.quaternion,
@ -365,12 +365,28 @@ class Rotation:
def flatten(self,order = 'C'): def flatten(self,order = 'C'):
"""Flatten array.""" """
Flatten array.
Returns
-------
flattened : damask.Rotation
Rotation flattened to single dimension.
"""
return self.copy(rotation=self.quaternion.reshape((-1,4),order=order)) return self.copy(rotation=self.quaternion.reshape((-1,4),order=order))
def reshape(self,shape,order = 'C'): def reshape(self,shape,order = 'C'):
"""Reshape array.""" """
Reshape array.
Returns
-------
reshaped : damask.Rotation
Rotation of given shape.
"""
if isinstance(shape,(int,np.integer)): shape = (shape,) if isinstance(shape,(int,np.integer)): shape = (shape,)
return self.copy(rotation=self.quaternion.reshape(tuple(shape)+(4,),order=order)) return self.copy(rotation=self.quaternion.reshape(tuple(shape)+(4,),order=order))
@ -387,6 +403,11 @@ class Rotation:
Where to preferentially locate missing dimensions. Where to preferentially locate missing dimensions.
Either 'left' or 'right' (default). Either 'left' or 'right' (default).
Returns
-------
broadcasted : damask.Rotation
Rotation broadcasted to given shape.
""" """
if isinstance(shape,(int,np.integer)): shape = (shape,) if isinstance(shape,(int,np.integer)): shape = (shape,)
return self.copy(rotation=np.broadcast_to(self.quaternion.reshape(util.shapeshifter(self.shape,shape,mode)+(4,)), return self.copy(rotation=np.broadcast_to(self.quaternion.reshape(util.shapeshifter(self.shape,shape,mode)+(4,)),
@ -404,7 +425,7 @@ class Rotation:
Returns Returns
------- -------
average : Rotation average : damask.Rotation
Weighted average of original Rotation field. Weighted average of original Rotation field.
References References
@ -438,9 +459,14 @@ class Rotation:
Parameters Parameters
---------- ----------
other : Rotation other : damask.Rotation
Rotation to which the misorientation is computed. Rotation to which the misorientation is computed.
Returns
-------
g : damask.Rotation
Misorientation.
""" """
return other*~self return other*~self
@ -531,10 +557,10 @@ class Rotation:
Returns Returns
------- -------
rho : numpy.ndarray of shape (...,4) containing rho : numpy.ndarray of shape (...,4) containing
[n_1, n_2, n_3, tan(ω/2)], ǀnǀ = 1 and ω [0,π] [n_1, n_2, n_3, tan(ω/2)], ǀnǀ = 1 and ω [0,π]
unless compact == True: unless compact == True:
numpy.ndarray of shape (...,3) containing numpy.ndarray of shape (...,3) containing
tan(ω/2) [n_1, n_2, n_3], ω [0,π]. tan(ω/2) [n_1, n_2, n_3], ω [0,π].
""" """

View File

@ -49,7 +49,7 @@ class Table:
Returns Returns
------- -------
slice : Table slice : damask.Table
Sliced part of the Table. Sliced part of the Table.
Examples Examples
@ -157,7 +157,7 @@ class Table:
Parameters Parameters
---------- ----------
other : Table other : damask.Table
Table to compare against. Table to compare against.
rtol : float, optional rtol : float, optional
Relative tolerance of equality. Relative tolerance of equality.
@ -185,7 +185,7 @@ class Table:
Parameters Parameters
---------- ----------
other : Table other : damask.Table
Table to compare against. Table to compare against.
rtol : float, optional rtol : float, optional
Relative tolerance of equality. Relative tolerance of equality.
@ -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

@ -3,7 +3,6 @@ import multiprocessing as mp
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
import numpy.ma as ma
import vtk import vtk
from vtk.util.numpy_support import numpy_to_vtk as np_to_vtk from vtk.util.numpy_support import numpy_to_vtk as np_to_vtk
from vtk.util.numpy_support import numpy_to_vtkIdTypeArray as np_to_vtkIdTypeArray from vtk.util.numpy_support import numpy_to_vtkIdTypeArray as np_to_vtkIdTypeArray
@ -47,9 +46,14 @@ class VTK:
grid : iterable of int, len (3) grid : iterable of int, len (3)
Number of cells along each dimension. Number of cells along each dimension.
size : iterable of float, len (3) size : iterable of float, len (3)
Physical lengths along each dimension. Physical length along each dimension.
origin : iterable of float, len (3), optional origin : iterable of float, len (3), optional
Spatial origin coordinates. Coordinates of grid origin.
Returns
-------
new : damask.VTK
VTK-based geometry without nodal or cell data.
""" """
vtk_data = vtk.vtkRectilinearGrid() vtk_data = vtk.vtkRectilinearGrid()
@ -68,7 +72,7 @@ class VTK:
""" """
Create VTK of type vtk.vtkUnstructuredGrid. Create VTK of type vtk.vtkUnstructuredGrid.
This is the common type for FEM solver results. This is the common type for mesh solver results.
Parameters Parameters
---------- ----------
@ -80,6 +84,11 @@ class VTK:
cell_type : str cell_type : str
Name of the vtk.vtkCell subclass. Tested for TRIANGLE, QUAD, TETRA, and HEXAHEDRON. Name of the vtk.vtkCell subclass. Tested for TRIANGLE, QUAD, TETRA, and HEXAHEDRON.
Returns
-------
new : damask.VTK
VTK-based geometry without nodal or cell data.
""" """
vtk_nodes = vtk.vtkPoints() vtk_nodes = vtk.vtkPoints()
vtk_nodes.SetData(np_to_vtk(nodes)) vtk_nodes.SetData(np_to_vtk(nodes))
@ -110,6 +119,11 @@ class VTK:
points : numpy.ndarray of shape (:,3) points : numpy.ndarray of shape (:,3)
Spatial position of the points. Spatial position of the points.
Returns
-------
new : damask.VTK
VTK-based geometry without nodal or cell data.
""" """
N = points.shape[0] N = points.shape[0]
vtk_points = vtk.vtkPoints() vtk_points = vtk.vtkPoints()
@ -137,25 +151,30 @@ class VTK:
fname : str or pathlib.Path fname : str or pathlib.Path
Filename for reading. Valid extensions are .vtr, .vtu, .vtp, and .vtk. Filename for reading. Valid extensions are .vtr, .vtu, .vtp, and .vtk.
dataset_type : str, optional dataset_type : str, optional
Name of the vtk.vtkDataSet subclass when opening a .vtk file. Valid types are vtkRectilinearGrid, Name of the vtk.vtkDataSet subclass when opening a .vtk file.
vtkUnstructuredGrid, and vtkPolyData. Valid types are vtkRectilinearGrid, vtkUnstructuredGrid, and vtkPolyData.
Returns
-------
loaded : damask.VTK
VTK-based geometry from file.
""" """
if not os.path.isfile(fname): # vtk has a strange error handling if not os.path.isfile(fname): # vtk has a strange error handling
raise FileNotFoundError(f'no such file: {fname}') raise FileNotFoundError(f'No such file: {fname}')
ext = Path(fname).suffix ext = Path(fname).suffix
if ext == '.vtk' or dataset_type is not None: if ext == '.vtk' or dataset_type is not None:
reader = vtk.vtkGenericDataObjectReader() reader = vtk.vtkGenericDataObjectReader()
reader.SetFileName(str(fname)) reader.SetFileName(str(fname))
if dataset_type is None: if dataset_type is None:
raise TypeError('Dataset type for *.vtk file not given.') raise TypeError('Dataset type for *.vtk file not given.')
elif dataset_type.lower().endswith('rectilineargrid'): elif dataset_type.lower().endswith(('rectilineargrid','rectilinear_grid')):
reader.Update() reader.Update()
vtk_data = reader.GetRectilinearGridOutput() vtk_data = reader.GetRectilinearGridOutput()
elif dataset_type.lower().endswith('unstructuredgrid'): elif dataset_type.lower().endswith(('unstructuredgrid','unstructured_grid')):
reader.Update() reader.Update()
vtk_data = reader.GetUnstructuredGridOutput() vtk_data = reader.GetUnstructuredGridOutput()
elif dataset_type.lower().endswith('polydata'): elif dataset_type.lower().endswith(('polydata','poly_data')):
reader.Update() reader.Update()
vtk_data = reader.GetPolyDataOutput() vtk_data = reader.GetPolyDataOutput()
else: else:
@ -246,7 +265,7 @@ class VTK:
raise ValueError('No label defined for numpy.ndarray') raise ValueError('No label defined for numpy.ndarray')
N_data = data.shape[0] N_data = data.shape[0]
data_ = np.where(data.mask,data.fill_value,data) if isinstance(data,ma.MaskedArray) else\ data_ = np.where(data.mask,data.fill_value,data) if isinstance(data,np.ma.MaskedArray) else\
data data
d = np_to_vtk((data_.astype(np.single) if data_.dtype in [np.double, np.longdouble] else d = np_to_vtk((data_.astype(np.single) if data_.dtype in [np.double, np.longdouble] else
data_).reshape(N_data,-1),deep=True) # avoid large files data_).reshape(N_data,-1),deep=True) # avoid large files
@ -277,6 +296,11 @@ class VTK:
label : str label : str
Data label. Data label.
Returns
-------
data : numpy.ndarray
Data stored under the given label.
""" """
cell_data = self.vtk_data.GetCellData() cell_data = self.vtk_data.GetCellData()
for a in range(cell_data.GetNumberOfArrays()): for a in range(cell_data.GetNumberOfArrays()):

View File

@ -1,15 +1,14 @@
""" """
Filters for operations on regular grids. Filters for operations on regular grids.
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 +28,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 +41,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 +139,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 +165,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 +197,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 +218,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 +240,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)
@ -220,6 +261,11 @@ def cellsSizeOrigin_coordinates0_point(coordinates0,ordered=True):
Expect coordinates0 data to be ordered (x fast, z slow). Expect coordinates0 data to be ordered (x fast, z slow).
Defaults to True. Defaults to True.
Returns
-------
cells, size, origin : Three numpy.ndarray, each of shape (3)
Information to reconstruct grid.
""" """
coords = [_np.unique(coordinates0[:,i]) for i in range(3)] coords = [_np.unique(coordinates0[:,i]) for i in range(3)]
mincorner = _np.array(list(map(min,coords))) mincorner = _np.array(list(map(min,coords)))
@ -252,19 +298,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 +311,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 +334,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 +354,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 +375,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 +397,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.
@ -378,6 +418,11 @@ def cellsSizeOrigin_coordinates0_node(coordinates0,ordered=True):
Expect coordinates0 data to be ordered (x fast, z slow). Expect coordinates0 data to be ordered (x fast, z slow).
Defaults to True. Defaults to True.
Returns
-------
cells, size, origin : Three numpy.ndarray, each of shape (3)
Information to reconstruct grid.
""" """
coords = [_np.unique(coordinates0[:,i]) for i in range(3)] coords = [_np.unique(coordinates0[:,i]) for i in range(3)]
mincorner = _np.array(list(map(min,coords))) mincorner = _np.array(list(map(min,coords)))
@ -402,6 +447,72 @@ 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 form a regular grid.
Parameters
----------
coordinates0 : numpy.ndarray
Array of undeformed cell coordinates.
Returns
-------
valid : bool
Whether the coordinates form a regular grid.
"""
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,9 @@
"""Finite-strain continuum mechanics.""" """
Finite-strain continuum mechanics.
All routines 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 +159,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
-------
coords : numpy.ndarray of shape (N_seeds,3)
Seed 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
-------
coords : numpy.ndarray of shape (N_seeds,3)
Seed 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
-------
coords, materials : numpy.ndarray of shape (:,3), numpy.ndarray of shape (:)
Seed coordinates in 3D space, material IDs.
""" """
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

@ -1,3 +1,3 @@
"""Tools to control the various solvers.""" """Run simulations directly from Python."""
from ._marc import Marc # noqa from ._marc import Marc # noqa

View File

@ -1,10 +1,7 @@
""" """
Tensor operations. Tensor mathematics.
Notes All routines operate on numpy.ndarrays of shape (...,3,3).
-----
This is not a tensor class, but a collection of routines
to operate on numpy.ndarrays of shape (...,3,3).
""" """

View File

@ -49,7 +49,7 @@ _colors = {
#################################################################################################### ####################################################################################################
def srepr(arg,glue = '\n'): def srepr(arg,glue = '\n'):
r""" r"""
Join arguments with glue string. Join items with glue string.
Parameters Parameters
---------- ----------
@ -58,6 +58,11 @@ def srepr(arg,glue = '\n'):
glue : str, optional glue : str, optional
Glue used for joining operation. Defaults to \n. Glue used for joining operation. Defaults to \n.
Returns
-------
joined : str
String representation of the joined items.
""" """
if (not hasattr(arg, 'strip') and if (not hasattr(arg, 'strip') and
(hasattr(arg, '__getitem__') or (hasattr(arg, '__getitem__') or
@ -68,19 +73,71 @@ def srepr(arg,glue = '\n'):
def emph(what): def emph(what):
"""Formats string with emphasis.""" """
Format with emphasis.
Parameters
----------
what : object with __repr__ or iterable of objects with __repr__.
Message to format.
Returns
-------
formatted : str
Formatted string representation of the joined items.
"""
return _colors['bold']+srepr(what)+_colors['end_color'] return _colors['bold']+srepr(what)+_colors['end_color']
def deemph(what): def deemph(what):
"""Formats string with deemphasis.""" """
Format with deemphasis.
Parameters
----------
what : object with __repr__ or iterable of objects with __repr__.
Message to format.
Returns
-------
formatted : str
Formatted string representation of the joined items.
"""
return _colors['dim']+srepr(what)+_colors['end_color'] return _colors['dim']+srepr(what)+_colors['end_color']
def warn(what): def warn(what):
"""Formats string for warning.""" """
Format for warning.
Parameters
----------
what : object with __repr__ or iterable of objects with __repr__.
Message to format.
Returns
-------
formatted : str
Formatted string representation of the joined items.
"""
return _colors['warning']+emph(what)+_colors['end_color'] return _colors['warning']+emph(what)+_colors['end_color']
def strikeout(what): def strikeout(what):
"""Formats string as strikeout.""" """
Format as strikeout.
Parameters
----------
what : object with __repr__ or iterable of objects with __repr__.
Message to format.
Returns
-------
formatted : str
Formatted string representation of the joined items.
"""
return _colors['crossout']+srepr(what)+_colors['end_color'] return _colors['crossout']+srepr(what)+_colors['end_color']
@ -97,6 +154,11 @@ def execute(cmd,wd='./',env=None):
env : dict, optional env : dict, optional
Environment for execution. Environment for execution.
Returns
-------
stdout, stderr : str
Output of the executed command.
""" """
print(f"executing '{cmd}' in '{wd}'") print(f"executing '{cmd}' in '{wd}'")
process = subprocess.run(shlex.split(cmd), process = subprocess.run(shlex.split(cmd),
@ -157,6 +219,11 @@ def scale_to_coprime(v):
v : numpy.ndarray of shape (:) v : numpy.ndarray of shape (:)
Vector to scale. Vector to scale.
Returns
-------
m : numpy.ndarray of shape (:)
Vector scaled to co-prime numbers.
""" """
MAX_DENOMINATOR = 1000000 MAX_DENOMINATOR = 1000000
@ -371,9 +438,14 @@ def DREAM3D_base_group(fname):
Parameters Parameters
---------- ----------
fname : str fname : str or pathlib.Path
Filename of the DREAM.3D (HDF5) file. Filename of the DREAM.3D (HDF5) file.
Returns
-------
path : str
Path to the base group.
""" """
with h5py.File(fname,'r') as f: with h5py.File(fname,'r') as f:
base_group = f.visit(lambda path: path.rsplit('/',2)[0] if '_SIMPL_GEOMETRY/SPACING' in path else None) base_group = f.visit(lambda path: path.rsplit('/',2)[0] if '_SIMPL_GEOMETRY/SPACING' in path else None)
@ -393,9 +465,14 @@ def DREAM3D_cell_data_group(fname):
Parameters Parameters
---------- ----------
fname : str fname : str or pathlib.Path
Filename of the DREAM.3D (HDF5) file. Filename of the DREAM.3D (HDF5) file.
Returns
-------
path : str
Path to the cell data group.
""" """
base_group = DREAM3D_base_group(fname) base_group = DREAM3D_base_group(fname)
with h5py.File(fname,'r') as f: with h5py.File(fname,'r') as f:

View File

@ -110,14 +110,14 @@ class TestResult:
def test_add_calculation(self,default,tmp_path,mode): def test_add_calculation(self,default,tmp_path,mode):
if mode == 'direct': if mode == 'direct':
default.add_calculation('x','2.0*np.abs(#F#)-1.0','-','my notes') default.add_calculation('2.0*np.abs(#F#)-1.0','x','-','my notes')
else: else:
with open(tmp_path/'f.py','w') as f: with open(tmp_path/'f.py','w') as f:
f.write("import numpy as np\ndef my_func(field):\n return 2.0*np.abs(field)-1.0\n") f.write("import numpy as np\ndef my_func(field):\n return 2.0*np.abs(field)-1.0\n")
sys.path.insert(0,str(tmp_path)) sys.path.insert(0,str(tmp_path))
import f import f
default.enable_user_function(f.my_func) default.enable_user_function(f.my_func)
default.add_calculation('x','my_func(#F#)','-','my notes') default.add_calculation('my_func(#F#)','x','-','my notes')
in_memory = 2.0*np.abs(default.place('F'))-1.0 in_memory = 2.0*np.abs(default.place('F'))-1.0
in_file = default.place('x') in_file = default.place('x')
@ -193,14 +193,14 @@ class TestResult:
def test_add_Mises_invalid(self,default): def test_add_Mises_invalid(self,default):
default.add_stress_Cauchy('P','F') default.add_stress_Cauchy('P','F')
default.add_calculation('sigma_y','#sigma#',unit='y') default.add_calculation('#sigma#','sigma_y',unit='y')
default.add_equivalent_Mises('sigma_y') default.add_equivalent_Mises('sigma_y')
assert default.get('sigma_y_vM') is None assert default.get('sigma_y_vM') is None
def test_add_Mises_stress_strain(self,default): def test_add_Mises_stress_strain(self,default):
default.add_stress_Cauchy('P','F') default.add_stress_Cauchy('P','F')
default.add_calculation('sigma_y','#sigma#',unit='y') default.add_calculation('#sigma#','sigma_y',unit='y')
default.add_calculation('sigma_x','#sigma#',unit='x') default.add_calculation('#sigma#','sigma_x',unit='x')
default.add_equivalent_Mises('sigma_y',kind='strain') default.add_equivalent_Mises('sigma_y',kind='strain')
default.add_equivalent_Mises('sigma_x',kind='stress') default.add_equivalent_Mises('sigma_x',kind='stress')
assert not np.allclose(default.place('sigma_y_vM'),default.place('sigma_x_vM')) assert not np.allclose(default.place('sigma_y_vM'),default.place('sigma_x_vM'))
@ -271,7 +271,7 @@ class TestResult:
@pytest.mark.parametrize('overwrite',['off','on']) @pytest.mark.parametrize('overwrite',['off','on'])
def test_add_overwrite(self,default,overwrite): def test_add_overwrite(self,default,overwrite):
last = default.view('times',default.times_in_range(0,np.inf)[-1]) last = default.view('increments',-1)
last.add_stress_Cauchy() last.add_stress_Cauchy()
@ -279,13 +279,13 @@ class TestResult:
created_first = datetime.strptime(created_first,'%Y-%m-%d %H:%M:%S%z') created_first = datetime.strptime(created_first,'%Y-%m-%d %H:%M:%S%z')
if overwrite == 'on': if overwrite == 'on':
last = last.allow_modification() last = last.modification_enable()
else: else:
last = last.disallow_modification() last = last.modification_disable()
time.sleep(2.) time.sleep(2.)
try: try:
last.add_calculation('sigma','#sigma#*0.0+311.','not the Cauchy stress') last.add_calculation('#sigma#*0.0+311.','sigma','not the Cauchy stress')
except ValueError: except ValueError:
pass pass
@ -301,14 +301,24 @@ class TestResult:
def test_rename(self,default,allowed): def test_rename(self,default,allowed):
if allowed == 'on': if allowed == 'on':
F = default.place('F') F = default.place('F')
default = default.allow_modification() default = default.modification_enable()
default.rename('F','new_name') default.rename('F','new_name')
assert np.all(F == default.place('new_name')) assert np.all(F == default.place('new_name'))
default = default.disallow_modification() default = default.modification_disable()
with pytest.raises(PermissionError): with pytest.raises(PermissionError):
default.rename('P','another_new_name') default.rename('P','another_new_name')
@pytest.mark.parametrize('allowed',['off','on'])
def test_remove(self,default,allowed):
if allowed == 'on':
unsafe = default.modification_enable()
unsafe.remove('F')
assert unsafe.get('F') is None
else:
with pytest.raises(PermissionError):
default.remove('F')
@pytest.mark.parametrize('mode',['cell','node']) @pytest.mark.parametrize('mode',['cell','node'])
def test_coordinates(self,default,mode): def test_coordinates(self,default,mode):
if mode == 'cell': if mode == 'cell':
@ -352,7 +362,7 @@ class TestResult:
def test_XDMF(self,tmp_path,single_phase,update,ref_path): def test_XDMF(self,tmp_path,single_phase,update,ref_path):
for shape in [('scalar',()),('vector',(3,)),('tensor',(3,3)),('matrix',(12,))]: for shape in [('scalar',()),('vector',(3,)),('tensor',(3,3)),('matrix',(12,))]:
for dtype in ['f4','f8','i1','i2','i4','i8','u1','u2','u4','u8']: for dtype in ['f4','f8','i1','i2','i4','i8','u1','u2','u4','u8']:
single_phase.add_calculation(f'{shape[0]}_{dtype}',f"np.ones(np.shape(#F#)[0:1]+{shape[1]},'{dtype}')") single_phase.add_calculation(f"np.ones(np.shape(#F#)[0:1]+{shape[1]},'{dtype}')",f'{shape[0]}_{dtype}')
fname = os.path.splitext(os.path.basename(single_phase.fname))[0]+'.xdmf' fname = os.path.splitext(os.path.basename(single_phase.fname))[0]+'.xdmf'
os.chdir(tmp_path) os.chdir(tmp_path)
single_phase.save_XDMF() single_phase.save_XDMF()

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):