Skip to content

Commit eb09abc

Browse files
authored
Voronoi foams (#5)
1 parent d1e5342 commit eb09abc

File tree

5 files changed

+326
-210
lines changed

5 files changed

+326
-210
lines changed

MSUtils/lattices/lattice_image.py

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,66 @@
1515

1616

1717
def physical_to_voxel(point, dimensions, shape):
18-
return np.round(point / dimensions * (np.array(shape) - 1)).astype(int)
18+
"""
19+
Map a physical coordinate in [0, L] to voxel index in [0, N-1],
20+
consistently with spacing = L/(N-1).
21+
"""
22+
point = np.asarray(point, dtype=np.float64)
23+
shape = np.asarray(shape, dtype=np.int64)
24+
dimensions = np.asarray(dimensions, dtype=np.float64)
25+
26+
# Map [0, L] -> [0, N-1], then clamp
27+
idx = np.floor(point / dimensions * (shape - 1) + 0.5).astype(np.int64)
28+
idx = np.clip(idx, 0, shape - 1)
29+
return idx
30+
31+
32+
def _inside_cell(P, L, eps=1e-12):
33+
P = np.asarray(P, float)
34+
L = np.asarray(L, float)
35+
return np.all(P >= -eps) and np.all(P < L + eps)
1936

2037

2138
def draw_strut(microstructure, start, end, radius, voxel_sizes, strut_type, L):
22-
start = np.array(start)
23-
end = np.array(end)
39+
start = np.asarray(start, np.float64)
40+
end = np.asarray(end, np.float64)
41+
L = np.asarray(L, np.float64)
42+
2443
direction = end - start
2544
length = np.linalg.norm(direction)
45+
if length == 0:
46+
return
2647
direction /= length
2748

28-
num_points = int(length / np.linalg.norm(voxel_sizes))
29-
points = np.linspace(0, length, num_points)
30-
points = start + np.outer(points, direction)
49+
step = np.linalg.norm(voxel_sizes) # ~ one voxel diagonal
50+
num_points = max(1, int(np.ceil(length / step)))
51+
ts = np.linspace(0.0, length, num_points, dtype=np.float64)
3152

32-
voxel_radius = [radius / voxel_sizes[i] for i in range(3)]
53+
voxel_radius = np.array([radius / voxel_sizes[i] for i in range(3)], np.float64)
3354

34-
for point in points:
35-
voxel_point = physical_to_voxel(point, L, microstructure.shape)
36-
x, y, z = voxel_point
37-
x_min, x_max = (
38-
max(0, int(x - voxel_radius[0])),
39-
min(microstructure.shape[0], int(x + voxel_radius[0] + 1)),
40-
)
41-
y_min, y_max = (
42-
max(0, int(y - voxel_radius[1])),
43-
min(microstructure.shape[1], int(y + voxel_radius[1] + 1)),
44-
)
45-
z_min, z_max = (
46-
max(0, int(z - voxel_radius[2])),
47-
min(microstructure.shape[2], int(z + voxel_radius[2] + 1)),
48-
)
55+
for t in ts:
56+
p = start + t * direction
57+
if not _inside_cell(p, L):
58+
continue
59+
60+
x, y, z = physical_to_voxel(p, L, microstructure.shape)
61+
62+
x_min = max(0, int(np.floor(x - voxel_radius[0])))
63+
x_max = min(microstructure.shape[0], int(np.ceil(x + voxel_radius[0] + 1)))
64+
y_min = max(0, int(np.floor(y - voxel_radius[1])))
65+
y_max = min(microstructure.shape[1], int(np.ceil(y + voxel_radius[1] + 1)))
66+
z_min = max(0, int(np.floor(z - voxel_radius[2])))
67+
z_max = min(microstructure.shape[2], int(np.ceil(z + voxel_radius[2] + 1)))
4968

5069
if strut_type == "circle":
5170
xx, yy, zz = np.ogrid[x_min:x_max, y_min:y_max, z_min:z_max]
52-
distance = (
53-
((xx - x) * voxel_sizes[0]) ** 2
54-
+ ((yy - y) * voxel_sizes[1]) ** 2
55-
+ ((zz - z) * voxel_sizes[2]) ** 2
56-
)
57-
mask = distance <= radius**2
58-
59-
microstructure[x_min:x_max, y_min:y_max, z_min:z_max][mask] = 1
71+
dx = (xx - x) * voxel_sizes[0]
72+
dy = (yy - y) * voxel_sizes[1]
73+
dz = (zz - z) * voxel_sizes[2]
74+
mask = (dx * dx + dy * dy + dz * dz) <= (radius * radius)
75+
microstructure[x_min:x_max, y_min:y_max, z_min:z_max][mask] = 1
76+
else:
77+
raise ValueError("Only 'circle' implemented.")
6078

6179

6280
def create_lattice_image(
@@ -76,14 +94,22 @@ def create_lattice_image(
7694
- microstructure: ndarray - The generated microstructure image.
7795
"""
7896
if L is None:
79-
L = [1, 1, 1]
97+
L = [1.0, 1.0, 1.0]
98+
L = np.asarray(L, dtype=np.float64)
8099

81100
vertices, edges = unit_cell_func()
82-
voxel_sizes = [L[i] / [Nx, Ny, Nz][i] for i in range(3)]
101+
102+
# CONSISTENT voxel spacing with 0..N-1 indexing
103+
Nvec = np.array([Nx, Ny, Nz], dtype=np.int64)
104+
voxel_sizes = L / (Nvec - 1).astype(np.float64)
83105

84106
microstructure = np.zeros((Nx, Ny, Nz), dtype=np.int8)
85-
for edge in edges:
86-
start, end = vertices[edge[0]] * L, vertices[edge[1]] * L
107+
108+
# scale vertices into physical domain [0, L]
109+
phys_vertices = vertices * L
110+
111+
for a, b in edges:
112+
start, end = phys_vertices[a], phys_vertices[b]
87113
draw_strut(microstructure, start, end, radius, voxel_sizes, strut_type, L)
88114

89115
return microstructure
@@ -128,7 +154,7 @@ def create_lattice_image(
128154
write_xdmf(
129155
h5_filepath="data/lattice_microstructures.h5",
130156
xdmf_filepath="data/lattice_microstructures.xdmf",
131-
microstructure_length=L,
157+
microstructure_length=L[::-1],
132158
time_series=False,
133159
verbose=True,
134160
)

MSUtils/voronoi/VoronoiSeeds.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,9 @@ def _generate_seeds(self) -> None:
128128
)
129129
case "diamond": # Diamond-like seed points
130130
self.num_crystals = 2
131-
self.seeds = np.array([[0, 0, 0], [0.5, 0.5, 0.5]])
131+
self.seeds = np.array([[0, 0, 0], [0.5, 0.5, 0.5]]) * np.array(
132+
self.RVE_length
133+
)
132134
case _:
133135
raise ValueError("Unknown sampling method! : " + self.method)
134136
# Add more sampling methods here if needed

MSUtils/voronoi/voronoi_foam.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import numpy as np
2+
from MSUtils.lattices.lattice_image import draw_strut, _inside_cell
3+
from MSUtils.voronoi.VoronoiTessellation import PeriodicVoronoiTessellation
4+
from MSUtils.voronoi.VoronoiSeeds import VoronoiSeeds
5+
from MSUtils.general.MicrostructureImage import MicrostructureImage
6+
from MSUtils.general.h52xdmf import write_xdmf
7+
8+
9+
def _all_voronoi_edges(tess):
10+
"""
11+
Extract all edges from a PeriodicVoronoiTessellation object.
12+
Each edge is represented as a tuple of its two endpoints.
13+
"""
14+
vor = tess.voronoi
15+
edges = []
16+
for verts in vor.ridge_vertices:
17+
if any(v == -1 for v in verts):
18+
continue
19+
if len(verts) < 2:
20+
continue
21+
cyc = list(verts) + [verts[0]]
22+
for u, v in zip(cyc[:-1], cyc[1:]):
23+
edges.append((vor.vertices[u].astype(float), vor.vertices[v].astype(float)))
24+
return edges
25+
26+
27+
def periodic_voronoi_foam(
28+
tess, N, L, strut_radius, dtype=np.uint8, strut_type="circle"
29+
):
30+
"""
31+
Use FULL Voronoi (with duplicated seeds) and draw every edge whose
32+
start or end lies inside [0,L)^d. No periodic translations needed.
33+
"""
34+
N = np.asarray(N, int)
35+
L = np.asarray(L, float)
36+
37+
micro = np.zeros(tuple(N.tolist()), dtype=dtype)
38+
voxel_sizes = L / (N - 1).astype(float)
39+
40+
edges = _all_voronoi_edges(tess)
41+
42+
# De-dup to avoid overdrawing: hash by midpoint & direction (sign-agnostic)
43+
seen = set()
44+
45+
for V0, V1 in edges:
46+
if not (_inside_cell(V0, L) or _inside_cell(V1, L)):
47+
continue
48+
49+
mid = 0.5 * (V0 + V1)
50+
d = V1 - V0
51+
nrm = np.linalg.norm(d) + 1e-15
52+
diru = np.round(d / nrm, 6)
53+
key = tuple(np.round(mid / np.maximum(L, 1.0), 6)) + tuple(
54+
np.minimum(diru, -diru)
55+
)
56+
if key in seen:
57+
continue
58+
seen.add(key)
59+
60+
draw_strut(micro, V0, V1, strut_radius, voxel_sizes, strut_type, L)
61+
62+
return micro
63+
64+
65+
if __name__ == "__main__":
66+
67+
num_crystals = 125
68+
L = [1, 1, 1]
69+
Nx, Ny, Nz = 512, 512, 512
70+
permute_order = "zyx"
71+
strut_radius = 0.01
72+
73+
# Generate Voronoi seeds and tessellation
74+
SeedInfo = VoronoiSeeds(num_crystals, L, "sobol", BitGeneratorSeed=42)
75+
tess = PeriodicVoronoiTessellation(L, SeedInfo.seeds)
76+
77+
# Generate Voronoi foam image
78+
micro = periodic_voronoi_foam(
79+
tess, [Nx, Ny, Nz], L, strut_radius, strut_type="circle"
80+
)
81+
82+
IMG = MicrostructureImage(image=micro, L=L)
83+
IMG.write("data/voro_foam.h5", "/dset_0", order=permute_order)
84+
85+
write_xdmf(
86+
"data/voro_foam.h5",
87+
"data/voro_foam.xdmf",
88+
microstructure_length=L[::-1],
89+
)

0 commit comments

Comments
 (0)