numpy_to_vtk
@author: Alexandre Sac–Morane alexandre.sac-morane@enpc.fr
This is the file to compute the geometrical tortuosity with a fast marching method.
Expand source code
import numpy as np
import heapq
from math import sqrt
def _neighbour_offsets(neighborhood, dx=1.0):
if neighborhood == 6:
return [((1,0,0), dx), ((-1,0,0), dx), ((0,1,0), dx), ((0,-1,0), dx), ((0,0,1), dx), ((0,0,-1), dx)]
# 26-neighborhood: all non-zero combos in {-1,0,1}^3
offs = []
for di in (-1,0,1):
for dj in (-1,0,1):
for dk in (-1,0,1):
if di==0 and dj==0 and dk==0:
continue
dist = dx * sqrt(di*di + dj*dj + dk*dk)
offs.append(((di,dj,dk), dist))
return offs
def compute_tortuosity_fast_marching(pore, extraction, dx=1.0, neighborhood=6):
"""
Compute geometrical tortuosity using a discrete fast-marching (Dijkstra) on a 3D grid.
Method:
- Treat `pore` as a boolean array (True=pore, False=solid).
- Initialize all pore voxels on the x=0 face as sources (distance 0).
- Run multi-source Dijkstra over pore voxels (neighbourhood 6 or 26).
- For pore voxels on the x=NX-1 face (outlet) collect shortest distances.
- Geometrical tortuosity tau = mean(distance_at_outlet) / L, where L = (NX-1)*dx.
Returns: (tau, mean_path_length)
"""
pore = np.asarray(pore, dtype=bool)
if pore.ndim != 3:
raise ValueError("pore must be a 3D array")
Nx, Ny, Nz = pore.shape
L = (Nx-1) * float(dx)
# Prepare offsets
offsets = _neighbour_offsets(neighborhood, dx=dx)
# Distance array
inf = float('inf')
dist = np.full(pore.shape, inf, dtype=float)
visited = np.zeros(pore.shape, dtype=bool)
# Multi-source initialization: all pore voxels at x=0
heap = []
for j in range(Ny):
for k in range(Nz):
if pore[0, j, k]:
dist[0, j, k] = 0.0
heapq.heappush(heap, (0.0, 0, j, k))
# Dijkstra loop
while heap:
d, i, j, k = heapq.heappop(heap)
if visited[i, j, k]:
continue
visited[i, j, k] = True
# relax neighbours
for (di,dj,dk), cost in offsets:
ni, nj, nk = i + di, j + dj, k + dk
if not (0 <= ni < Nx and 0 <= nj < Ny and 0 <= nk < Nz):
continue
if not pore[ni, nj, nk]:
continue
if visited[ni, nj, nk]:
continue
nd = d + cost
if nd < dist[ni, nj, nk]:
dist[ni, nj, nk] = nd
heapq.heappush(heap, (nd, ni, nj, nk))
# Collect outlet distances (x = Nx-1)
outlet_mask = pore[Nx-1, extraction[0]:extraction[1], extraction[2]:extraction[3]]
outlet_dists = dist[Nx-1, extraction[0]:extraction[1], extraction[2]:extraction[3]][outlet_mask]
if outlet_dists.size == 0:
raise RuntimeError("No reachable outlet pores found. Check connectivity or BCs.")
mean_path = float(np.mean(outlet_dists))
tau = mean_path / L
return tau
if __name__ == '__main__':
# Quick self-test on a straight channel: tortuosity should be ~1.0
nx, ny, nz = 50, 10, 10
pore = np.zeros((nx, ny, nz), dtype=bool)
pore[:, :, :] = False
# straight open channel
pore[:, 4:6, 4:6] = True
tau = compute_tortuosity_fast_marching(pore, extraction=[0, ny, 0, nz], dx=1.0, neighborhood=6)
print(f"tau={tau:.5f} (expected =1.0)")
def _neighbour_offsets()
Compute offsets for a given neighborhood size.
Expand source code
def _neighbour_offsets(neighborhood, dx=1.0):
if neighborhood == 6:
return [((1,0,0), dx), ((-1,0,0), dx), ((0,1,0), dx), ((0,-1,0), dx), ((0,0,1), dx), ((0,0,-1), dx)]
# 26-neighborhood: all non-zero combos in {-1,0,1}^3
offs = []
for di in (-1,0,1):
for dj in (-1,0,1):
for dk in (-1,0,1):
if di==0 and dj==0 and dk==0:
continue
dist = dx * sqrt(di*di + dj*dj + dk*dk)
offs.append(((di,dj,dk), dist))
return offs
def compute_tortuosity_fast_marching()
Compute geometrical tortuosity using a discrete fast-marching (Dijkstra) on a 3D grid.
Method:
- Treat `pore` as a boolean array (True=pore, False=solid).
- Initialize all pore voxels on the x=0 face as sources (distance 0).
- Run multi-source Dijkstra over pore voxels (neighbourhood 6 or 26).
- For pore voxels on the x=NX-1 face (outlet) collect shortest distances.
- Geometrical tortuosity tau = mean(distance_at_outlet) / L, where L = (NX-1)*dx.
Returns: (tau, mean_path_length)
Expand source code
def compute_tortuosity_fast_marching(pore, extraction, dx=1.0, neighborhood=6):
"""
Compute geometrical tortuosity using a discrete fast-marching (Dijkstra) on a 3D grid.
Method:
- Treat `pore` as a boolean array (True=pore, False=solid).
- Initialize all pore voxels on the x=0 face as sources (distance 0).
- Run multi-source Dijkstra over pore voxels (neighbourhood 6 or 26).
- For pore voxels on the x=NX-1 face (outlet) collect shortest distances.
- Geometrical tortuosity tau = mean(distance_at_outlet) / L, where L = (NX-1)*dx.
Returns: (tau, mean_path_length)
"""
pore = np.asarray(pore, dtype=bool)
if pore.ndim != 3:
raise ValueError("pore must be a 3D array")
Nx, Ny, Nz = pore.shape
L = (Nx-1) * float(dx)
# Prepare offsets
offsets = _neighbour_offsets(neighborhood, dx=dx)
# Distance array
inf = float('inf')
dist = np.full(pore.shape, inf, dtype=float)
visited = np.zeros(pore.shape, dtype=bool)
# Multi-source initialization: all pore voxels at x=0
heap = []
for j in range(Ny):
for k in range(Nz):
if pore[0, j, k]:
dist[0, j, k] = 0.0
heapq.heappush(heap, (0.0, 0, j, k))
# Dijkstra loop
while heap:
d, i, j, k = heapq.heappop(heap)
if visited[i, j, k]:
continue
visited[i, j, k] = True
# relax neighbours
for (di,dj,dk), cost in offsets:
ni, nj, nk = i + di, j + dj, k + dk
if not (0 <= ni < Nx and 0 <= nj < Ny and 0 <= nk < Nz):
continue
if not pore[ni, nj, nk]:
continue
if visited[ni, nj, nk]:
continue
nd = d + cost
if nd < dist[ni, nj, nk]:
dist[ni, nj, nk] = nd
heapq.heappush(heap, (nd, ni, nj, nk))
# Collect outlet distances (x = Nx-1)
outlet_mask = pore[Nx-1, extraction[0]:extraction[1], extraction[2]:extraction[3]]
outlet_dists = dist[Nx-1, extraction[0]:extraction[1], extraction[2]:extraction[3]][outlet_mask]
if outlet_dists.size == 0:
raise RuntimeError("No reachable outlet pores found. Check connectivity or BCs.")
mean_path = float(np.mean(outlet_dists))
tau = mean_path / L
return tau