kabs computes the absolute (Darcy) permeability of a porous material from its 3D tomographic image using the Lattice Boltzmann Method (LBM). Given a binary voxel image of the pore space, it solves single-phase incompressible creeping flow, returning results in lattice units or physical units.
The LBM implementation is adapted from Taichi-LBM3D (DOI) by Jianhui Yang.
git clone https://github.com/PMEAL/kabs.git
cd kabs
pip install -e .Key dependencies: taichi (GPU/CPU acceleration), numpy, pyevtk.
Optional: porespy (used in the examples below to generate synthetic images).
import taichi as ti
import porespy as ps
from kabs import solve_flow, compute_permeability
ti.init(arch=ti.cpu) # use ti.gpu for GPU acceleration
# Generate a synthetic test image (1 = pore, 0 = solid)
im = ps.generators.cylinders([200, 200, 200], r=10, porosity=0.7).astype(int)
# Run the LBM simulation. Saves result to "sample.vtr" when done.
solver = solve_flow(im, direction="x", export_vtk=False)
solver.export_VTK("sample")
# Compute permeability from the saved file
results = compute_permeability("sample.vtr", direction="x")
print(f"k = {results['k_lu']:.4e} voxels²")Taichi supports CUDA, Metal, and Vulkan backends. Switch by changing ti.init:
ti.init(arch=ti.gpu) # picks the best available GPU backend
ti.init(arch=ti.cuda) # CUDA explicitlyPass the voxel size dx_m (in metres) to compute_permeability to get results in
m² and milliDarcy:
results = compute_permeability(
"sample.vtr",
direction="x",
dx_m=2.85e-6, # 2.85-micron voxels, typical for micro-CT
)
print(f"k = {results['k_mD']:.2f} mD")
print(f"k = {results['k_m2']:.4e} m²")By default solve_flow stops early once the velocity field has converged to within a
relative tolerance of 1e-3 (i.e. delta|v| / |v| < 1e-3). The actual number of
steps run is printed and reflected in the auto-generated VTR filename.
# Tighten or loosen the tolerance
solver = solve_flow(im, direction="x", tol=1e-4) # tighter
solver = solve_flow(im, direction="x", tol=1e-2) # faster, coarser
# Disable early stopping and always run n_steps
n = 5000
solve_flow(im, direction="x", n_steps=n, tol=None, output_prefix="sample")
compute_permeability(f"sample-{n}-x.vtr", direction="x")The convergence check fires every log_every steps (default 500), so the true stopping
point is rounded to that interval.
For anisotropic materials, run all three directions:
for ax in ("x", "y", "z"):
solver = solve_flow(im, direction=ax, export_vtk=False)
solver.export_VTK(f"sample_{ax}")
kx = compute_permeability("sample_x.vtr", direction="x", dx_m=2.85e-6)
ky = compute_permeability("sample_y.vtr", direction="y", dx_m=2.85e-6)
kz = compute_permeability("sample_z.vtr", direction="z", dx_m=2.85e-6)
print(f"Kx={kx['k_mD']:.2f} Ky={ky['k_mD']:.2f} Kz={kz['k_mD']:.2f} mD")For images with a high solid fraction, enable sparse storage so only pore voxels are allocated in GPU memory:
solver = solve_flow(im, direction="x", sparse=True)compute_permeability returns a dict:
| Key | Description |
|---|---|
porosity |
Pore volume fraction (dimensionless) |
u_darcy |
Darcy (superficial) velocity [lattice units] |
u_pore |
Mean pore-space velocity [lattice units] |
k_lu |
Permeability in lattice units (voxels²) |
k_m2 |
Permeability in m² (None if dx_m not given) |
k_mD |
Permeability in milliDarcy (None if no dx_m) |
-
A pressure-driven flow is imposed by fixing density (ρ_in = 1.00, ρ_out = 0.99) on opposite faces of the domain along the chosen axis; the other four faces are periodic.
-
The D3Q19 MRT-LBM collision operator evolves the distribution functions to steady state. Solid voxels use bounce-back boundary conditions.
-
Darcy's law is applied to the converged velocity field:
K = u_D · μ / |∇P|
where u_D is the volume-averaged (Darcy) velocity and |∇P| = Δρ · c_s² / L with c_s² = 1/3 for D3Q19.
-
The result in lattice units is scaled to m² (or milliDarcy) using the physical voxel size dx_m.
