Skip to content

Commit b425098

Browse files
committed
Merge branch 'initial-condition-vectors' into 'development'
Initial condition vectors See merge request damask/DAMASK!1149
2 parents 86c748f + 3b4e8c2 commit b425098

File tree

53 files changed

+1080
-169
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1080
-169
lines changed

PRIVATE

python/damask/_geomgrid.py

Lines changed: 146 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import copy
3+
import numbers
34
import multiprocessing as mp
45
from functools import partial
56
from typing import Optional, Union, Sequence
@@ -17,14 +18,66 @@
1718
from . import Table
1819
from . import Colormap
1920
from ._typehints import FloatSequence, IntSequence, NumpyRngSeed
20-
try:
21-
import numba as nb # type: ignore[import-not-found]
22-
except ImportError:
23-
nb = False
2421

2522

26-
def numba_njit_wrapper(**kwargs):
27-
return (lambda function: nb.njit(function) if nb else function)
23+
class IcDict(dict):
24+
"""Dict that validates and broadcasts initial conditions on assignment."""
25+
26+
def __init__(self, cells: tuple[int, int, int]):
27+
"""
28+
New initial condition dictionary.
29+
30+
Parameters
31+
----------
32+
cells : tuple of int, len (3)
33+
Cell counts along x,y,z direction.
34+
"""
35+
super().__init__()
36+
self.cells = cells
37+
38+
def __setitem__(self, k: str, v: Union[int, float, np.ndarray]):
39+
"""
40+
Set a single initial condition key with validation and broadcasting.
41+
42+
Parameters
43+
----------
44+
k : str
45+
Name of the initial condition field.
46+
v : int, float, or np.ndarray
47+
Value to assign to the field. Allowed types and shapes:
48+
- Scalars (`int` or `float`)
49+
→ broadcast to `cells`.
50+
- Small arrays (`np.ndarray`) of shape (), (1,), or (3,)
51+
→ broadcast to `cells + value.shape`.
52+
- Full-field arrays (`np.ndarray`) with leading dimensions matching `cells` and trailing shape (), (1,), or (3,)
53+
→ copied as-is.
54+
55+
Raises
56+
------
57+
ValueError
58+
If `v` has an invalid shape that does not match the allowed rules.
59+
60+
Notes
61+
-----
62+
Broadcasting ensures that all initial condition fields conform to the
63+
grid defined by `cells`.
64+
This method is called both for full-property assignment (via the parent setter)
65+
and per-key assignment (`GeomGrid.initial_conditions[key] = value`), so all
66+
validation and broadcasting logic is centralized here.
67+
"""
68+
if not ( isinstance(v, numbers.Real)
69+
or (isinstance(v, np.ndarray) and v.shape in [(), (1,), (3,)])
70+
or (isinstance(v, np.ndarray) and len(v.shape) >= 3 and
71+
v.shape[:3] == self.cells and v.shape[3:] in [(), (1,), (3,)])
72+
):
73+
raise ValueError(f'initial condition "{k}" must be [a field of] scalars or three-dimensional vectors')
74+
75+
super().__setitem__(k,
76+
v if isinstance(v, np.ndarray) and len(v.shape) >= 3 and v.shape[:3] == self.cells else
77+
np.broadcast_to(v, self.cells + v.shape) if isinstance(v, np.ndarray) else
78+
np.broadcast_to(v, self.cells)
79+
)
80+
2881

2982
class GeomGrid:
3083
"""
@@ -56,12 +109,15 @@ def __init__(self,
56109
Coordinates of grid origin in meter. Defaults to [0.0,0.0,0.0].
57110
initial_conditions : dictionary, optional
58111
Initial condition label and field values at each grid point.
112+
If leading shape deviates from material shape,
113+
the (constant) value is broadcast across the grid.
59114
comments : (sequence of) str, optional
60115
Additional, human-readable information, e.g. history of operations.
61116
"""
62117
self.material = material
63-
self.size = size # type: ignore[assignment]
64-
self.origin = origin # type: ignore[assignment]
118+
self.size = size
119+
self.origin = origin
120+
self._ic = IcDict(tuple(self.cells))
65121
self.initial_conditions = {} if initial_conditions is None else initial_conditions
66122
self.comments = [] if comments is None else \
67123
[comments] if isinstance(comments,str) else \
@@ -82,7 +138,8 @@ def __repr__(self) -> str:
82138
f'origin: {util.srepr(self.origin," ")} m',
83139
f'# materials: {mat_N}' + ('' if mat_min == 0 and mat_max == mat_N-1 else
84140
f' (min: {mat_min}, max: {mat_max})')
85-
]+(['initial_conditions:']+[f' - {f}' for f in self.initial_conditions] if self.initial_conditions else []))
141+
]+(['initial_conditions:']+[f' - {f}'+(f' {data.shape[3:]}' if len(data.shape)>3 else '')
142+
for f,data in self.initial_conditions.items()] if self.initial_conditions else []))
86143

87144

88145
def __copy__(self) -> 'GeomGrid':
@@ -113,12 +170,40 @@ def __eq__(self,
113170
equal : bool
114171
Whether both arguments are equal.
115172
"""
173+
if not isinstance(other, GeomGrid):
174+
return NotImplemented
175+
return self.match(other) and bool( other.initial_conditions.keys() == self.initial_conditions.keys()
176+
and all(np.allclose(other.initial_conditions[k], self.initial_conditions[k])
177+
for k in self.initial_conditions)
178+
)
179+
180+
181+
def match(self,
182+
other: object) -> bool:
183+
"""
184+
Test geometric equality of other, i.e., ignoring initial conditions.
185+
186+
Parameters
187+
----------
188+
other : damask.GeomGrid
189+
GeomGrid to compare self against.
190+
191+
Returns
192+
-------
193+
match : bool
194+
Whether both arguments are geometrically equal.
195+
196+
Notes
197+
-----
198+
This comparison does not consider initial conditions.
199+
"""
116200
if not isinstance(other, GeomGrid):
117201
return NotImplemented
118202
return bool( np.allclose(other.size,self.size)
119203
and np.allclose(other.origin,self.origin)
120204
and np.all(other.cells == self.cells)
121-
and np.all(other.material == self.material))
205+
and np.all(other.material == self.material)
206+
)
122207

123208

124209
@property
@@ -149,7 +234,7 @@ def size(self) -> np.ndarray:
149234
@size.setter
150235
def size(self,
151236
size: FloatSequence):
152-
if len(size) != 3 or any(np.array(size) < 0):
237+
if len(size) != 3 or any(np.asarray(size) < 0):
153238
raise ValueError(f'invalid size {size}')
154239

155240
self._size = np.array(size,np.float64)
@@ -167,21 +252,22 @@ def origin(self,
167252

168253
self._origin = np.array(origin,np.float64)
169254

255+
170256
@property
171-
def initial_conditions(self) -> dict[str,np.ndarray]:
257+
def initial_conditions(self) -> dict[str, np.ndarray]:
172258
"""Fields of initial conditions."""
173-
self._ic = dict(zip(self._ic.keys(), # type: ignore[has-type]
174-
[v if isinstance(v,np.ndarray) else
175-
np.broadcast_to(v,self.cells) for v in self._ic.values()])) # type: ignore[has-type]
176259
return self._ic
177260

178261
@initial_conditions.setter
179-
def initial_conditions(self,
180-
ic: dict[str,np.ndarray]):
181-
if not isinstance(ic,dict):
182-
raise TypeError('initial conditions is not a dictionary')
262+
def initial_conditions(self, ic: dict[str, np.ndarray]):
263+
"""Set initial conditions, broadcasting to the cell grid shape if necessary."""
264+
if not isinstance(ic, dict):
265+
raise TypeError('initial conditions must be a dictionary')
266+
267+
self._ic.clear()
268+
for k, v in ic.items():
269+
self._ic[k] = v
183270

184-
self._ic = ic
185271

186272
@property
187273
def cells(self) -> np.ndarray:
@@ -214,9 +300,9 @@ def _load(fname: Union[str, Path], label: str) -> 'GeomGrid':
214300
Grid-based geometry from file.
215301
"""
216302
v = VTK.load(fname if str(fname).endswith('.vti') else str(fname)+'.vti')
217-
cells = np.array(v.vtk_data.GetDimensions())-1 # type: ignore[attr-defined]
303+
cells = tuple(np.array(v.vtk_data.GetDimensions())-1) # type: ignore[attr-defined]
218304
bbox = np.array(v.vtk_data.GetBounds()).reshape(3,2).T
219-
ic = {l:v.get(l).reshape(cells,order='F') for l in set(v.labels['Cell Data']) - {label}}
305+
ic = {l:v.get(l).reshape(cells+v.get(l).shape[1:],order='F') for l in set(v.labels['Cell Data']) - {label}}
220306

221307
return GeomGrid(material = v.get(label).reshape(cells,order='F'),
222308
size = bbox[1] - bbox[0],
@@ -714,7 +800,7 @@ def save(self,
714800
v = VTK.from_image_data(self.cells,self.size,self.origin)\
715801
.set('material',self.material.flatten(order='F'))
716802
for label,data in self.initial_conditions.items():
717-
v = v.set(label,data.flatten(order='F'))
803+
v = v.set(label,data.reshape((-1,)+data.shape[3:],order='F'))
718804
v.comments = self.comments
719805

720806
v.save(fname,parallel=False,compress=compress)
@@ -759,6 +845,10 @@ def canvas(self,
759845
updated : damask.GeomGrid
760846
Updated grid-based geometry.
761847
848+
Notes
849+
-----
850+
Existing initial condition fields will be removed.
851+
762852
Examples
763853
--------
764854
Remove lower 1/2 of the microstructure in z-direction.
@@ -839,22 +929,29 @@ def mirror(self,
839929
>>> g.mirror('xy') == g.mirror(['y','x'])
840930
True
841931
"""
842-
if not set(directions).issubset(valid := ['x', 'y', 'z']):
932+
valid = 'xyz'
933+
if not set(directions).issubset(valid):
843934
raise ValueError(f'invalid direction "{set(directions).difference(valid)}" specified')
844935

845936
limits: Sequence[Optional[int]] = [None,None] if reflect else [-2,0]
937+
selection = (slice(limits[0],limits[1],-1),slice(None),slice(None))
846938
mat = self.material.copy()
847-
848-
if 'x' in directions:
849-
mat = np.concatenate([mat,mat[limits[0]:limits[1]:-1,:,:]],0)
850-
if 'y' in directions:
851-
mat = np.concatenate([mat,mat[:,limits[0]:limits[1]:-1,:]],1)
852-
if 'z' in directions:
853-
mat = np.concatenate([mat,mat[:,:,limits[0]:limits[1]:-1]],2)
939+
ic = self.initial_conditions.copy()
940+
941+
for i,d in enumerate(valid):
942+
if d in directions:
943+
mat = np.concatenate([mat,
944+
mat[selection[0-i],selection[1-i],selection[2-i]]],
945+
axis=i)
946+
for label in ic:
947+
ic[label] = np.concatenate([ic[label],
948+
ic[label][selection[0-i],selection[1-i],selection[2-i]]],
949+
axis=i)
854950

855951
return GeomGrid(material = mat,
856952
size = self.size/self.cells*np.asarray(mat.shape),
857953
origin = self.origin,
954+
initial_conditions = ic,
858955
comments = self.comments+[util.execution_stamp('GeomGrid','mirror')],
859956
)
860957

@@ -893,14 +990,18 @@ def flip(self,
893990
>>> g.mirror('x',reflect=True) == g.mirror('x',reflect=True).flip('x')
894991
True
895992
"""
896-
if not set(directions).issubset(valid := ['x', 'y', 'z']):
993+
valid = 'xyz'
994+
if not set(directions).issubset(valid):
897995
raise ValueError(f'invalid direction "{set(directions).difference(valid)}" specified')
898996

899997
mat = np.flip(self.material, [valid.index(d) for d in directions if d in valid])
900-
998+
ic = {}
999+
for label in self.initial_conditions:
1000+
ic[label] = np.flip(self.initial_conditions[label],[valid.index(d) for d in directions if d in valid])
9011001
return GeomGrid(material = mat,
9021002
size = self.size,
9031003
origin = self.origin,
1004+
initial_conditions = ic,
9041005
comments = self.comments+[util.execution_stamp('GeomGrid','flip')],
9051006
)
9061007

@@ -924,6 +1025,10 @@ def rotate(self,
9241025
updated : damask.GeomGrid
9251026
Updated grid-based geometry.
9261027
1028+
Notes
1029+
-----
1030+
Existing initial condition fields will be removed.
1031+
9271032
Examples
9281033
--------
9291034
Rotation by 180° (π) is equivalent to twice flipping.
@@ -1023,7 +1128,8 @@ def assemble(self,
10231128
"""
10241129
cells = idx.shape[:3]
10251130
flat = (idx if len(idx.shape)==3 else grid_filters.ravel_index(idx)).flatten(order='F')
1026-
ic = {k: v.flatten(order='F')[flat].reshape(cells,order='F') for k,v in self.initial_conditions.items()}
1131+
ic = {k: v.reshape((-1,)+v.shape[3:],order='F')[flat]
1132+
.reshape(cells+v.shape[3:],order='F') for k,v in self.initial_conditions.items()}
10271133

10281134
return GeomGrid(material = self.material.flatten(order='F')[flat].reshape(cells,order='F'),
10291135
size = self.size,
@@ -1306,7 +1412,7 @@ def vicinity_offset(self,
13061412
updated : damask.GeomGrid
13071413
Updated grid-based geometry.
13081414
"""
1309-
@numba_njit_wrapper()
1415+
@util.numba_njit_wrapper()
13101416
def tainted_neighborhood(stencil: np.ndarray,
13111417
selection: Optional[np.ndarray] = None):
13121418
me = stencil[stencil.size//2]
@@ -1361,15 +1467,16 @@ def get_grain_boundaries(self,
13611467
grain_boundaries : damask.VTK
13621468
VTK-based geometry of grain boundary network.
13631469
"""
1364-
if not set(directions).issubset(valid := ['x', 'y', 'z']):
1470+
valid = 'xyz'
1471+
if not set(directions).issubset(valid):
13651472
raise ValueError(f'invalid direction "{set(directions).difference(valid)}" specified')
13661473

13671474
o = [[0, self.cells[0]+1, np.prod(self.cells[:2]+1)+self.cells[0]+1, np.prod(self.cells[:2]+1)],
13681475
[0, np.prod(self.cells[:2]+1), np.prod(self.cells[:2]+1)+1, 1],
13691476
[0, 1, self.cells[0]+1+1, self.cells[0]+1]] # offset for connectivity
13701477

13711478
connectivity = []
1372-
for i,d in enumerate(['x','y','z']):
1479+
for i,d in enumerate(valid):
13731480
if d not in directions: continue
13741481
mask = self.material != np.roll(self.material,1,i)
13751482
for j in [0,1,2]:
@@ -1378,8 +1485,8 @@ def get_grain_boundaries(self,
13781485
if i == 1 and not periodic: mask[:,0,:] = mask[:,-1,:] = False
13791486
if i == 2 and not periodic: mask[:,:,0] = mask[:,:,-1] = False
13801487

1381-
base_nodes = np.argwhere(mask.flatten(order='F')).reshape(-1,1)
1488+
base_nodes = np.argwhere(mask.flatten(order='F')).reshape((-1,1))
13821489
connectivity.append(np.block([base_nodes + o[i][k] for k in range(4)]))
13831490

1384-
coords = grid_filters.coordinates0_node(self.cells,self.size,self.origin).reshape(-1,3,order='F')
1491+
coords = grid_filters.coordinates0_node(self.cells,self.size,self.origin).reshape((-1,3),order='F')
13851492
return VTK.from_unstructured_grid(coords,np.vstack(connectivity),'QUADRILATERAL')

python/damask/_vtk.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
vtkImageData,
3030
vtkRectilinearGrid,
3131
vtkUnstructuredGrid,
32+
vtkPointData,
33+
vtkCellData,
3234
vtkPolyData,
3335
VTK_LAGRANGE_TRIANGLE,
3436
VTK_LAGRANGE_QUADRILATERAL,
@@ -597,25 +599,21 @@ def get(self,
597599
data : numpy.ndarray
598600
Data stored under the given label.
599601
"""
600-
cell_data = self.vtk_data.GetCellData()
601-
if label in [cell_data.GetArrayName(a) for a in range(cell_data.GetNumberOfArrays())]:
602-
try:
603-
return vtk_to_numpy(cell_data.GetArray(label))
604-
except AttributeError:
605-
vtk_array = cell_data.GetAbstractArray(label) # string array
602+
cell_data: vtkCellData = self.vtk_data.GetCellData()
603+
point_data: vtkPointData = self.vtk_data.GetPointData()
606604

607-
point_data = self.vtk_data.GetPointData()
608-
if label in [point_data.GetArrayName(a) for a in range(point_data.GetNumberOfArrays())]:
609-
try:
610-
return vtk_to_numpy(point_data.GetArray(label))
611-
except AttributeError:
612-
vtk_array = point_data.GetAbstractArray(label) # string array
605+
if label in [cell_data.GetArrayName(i) for i in range(cell_data.GetNumberOfArrays())]:
606+
vtk_container: Union[vtkPointData,vtkCellData] = cell_data
607+
elif label in [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())]:
608+
vtk_container = point_data
609+
else:
610+
raise KeyError(f'array "{label}" not found in cell or point data')
613611

614612
try:
615-
# string array
613+
return vtk_to_numpy(vtk_container.GetArray(label))
614+
except AttributeError: # assume string array
615+
vtk_array = vtk_container.GetAbstractArray(label)
616616
return np.array([vtk_array.GetValue(i) for i in range(vtk_array.GetNumberOfValues())]).astype(str)
617-
except UnboundLocalError:
618-
raise KeyError(f'array "{label}" not found')
619617

620618

621619
def delete(self,

0 commit comments

Comments
 (0)