Source code for porespy.networks._funcs

import logging

import numpy as np
import openpnm as op
import scipy.ndimage as spim
from skimage.morphology import ball, cube
from skimage.segmentation import find_boundaries

from porespy import settings
from porespy.generators import borders
from porespy.tools import get_tqdm, insert_cylinder, make_contiguous, overlay

__all__ = [
    "add_boundary_regions",
    "generate_voxel_image",
    "label_phases",
    "label_boundaries",
    "map_to_regions",
]


tqdm = get_tqdm()
logger = logging.getLogger(__name__)


[docs] def map_to_regions(regions, values): r""" Maps pore values from a network onto the image from which it was extracted Parameters ---------- regions : ndarray An image of the pore space partitioned into regions and labeled values : array_like An array containing the numerical values to insert into each region. The value at location *n* will be inserted into the image where ``regions`` is *n+1*. This mismatch is caused by the fact that 0's in the ``regions`` image is assumed to be the backgroung phase, while pore index 0 is valid. Notes ----- This function assumes that the array of pore values are indexed starting at location 0, while in the region image 0's indicate background phase and the region indexing starts at 1. That is, region 1 corresponds to pore 0. Examples -------- `Click here <https://porespy.org/examples/networks/reference/map_to_regions.html>`_ to view online example. """ values = np.array(values).flatten() if np.size(values) != regions.max(): raise Exception('Number of values does not match number of regions') im = np.zeros_like(regions) im = values[regions-1] im = im*(regions > 0) return im
[docs] def add_boundary_regions(regions, pad_width=3): r""" Add boundary regions on specified faces of an image Parameters ---------- regions : ndarray An image containing labelled regions, such as a watershed segmentation pad_width : array_like Number of layers to add to the beginning and end of each axis. This argument is handled similarly to the ``pad_width`` in the ``np.pad`` function. A scalar adds the same amount to the beginning and end of each axis. [A, B] adds A to the beginning of each axis and B to the ends. [[A, B], ..., [C, D]] adds A to the beginning and B to the end of the first axis, and so on. The default is to add 3 voxels on both ends of each axis. One exception is is [A, B, C] which A to the beginning and end of the first axis, and so on. This extra option is useful for putting 0 on some axes (i.e. [3, 0, 0]). Returns ------- padded_regions : ndarray An image with new regions padded on each side of the specified width. Examples -------- `Click here <https://porespy.org/examples/networks/reference/add_boundary_regions.html>`_ to view online example. """ # Parse user specified padding faces = np.array(pad_width) if faces.size == 1: faces = np.array([[faces, faces]]*regions.ndim) elif faces.size == regions.ndim: faces = np.vstack([faces]*2) else: pass t = faces.max() mx = regions.max() # Put a border around each region so padded regions are isolated bd = find_boundaries(regions, connectivity=regions.ndim, mode='inner') # Pad by t in all directions, this will be trimmed down later face_regions = np.pad(regions*(~bd), pad_width=t, mode='edge') # Set corners to 0 so regions don't connect across faces edges = borders(shape=face_regions.shape, mode='edges', thickness=t) face_regions[edges] = 0 # Extract a mask of just the faces mask = borders(shape=face_regions.shape, mode='faces', thickness=t) # Relabel regions on faces new_regions = spim.label(face_regions*mask)[0] + mx*(face_regions > 0) new_regions[~mask] = regions.flatten() # Trim image down to user specified size s = tuple([slice(t-ax[0], -(t-ax[1]) or None) for ax in faces]) new_regions = new_regions[s] new_regions = make_contiguous(new_regions) return new_regions
def _generate_voxel_image(network, pore_shape, throat_shape, max_dim=200): r""" Generates a 3d numpy array from an OpenPNM network Parameters ---------- network : OpenPNM GenericNetwork Network from which voxel image is to be generated pore_shape : str Shape of pores in the network, valid choices are "sphere", "cube" throat_shape : str Shape of throats in the network, valid choices are "cylinder", "cuboid" max_dim : int Number of voxels in the largest dimension of the network Returns ------- im : ndarray Voxelated image corresponding to the given pore network model Notes ----- (1) The generated voxel image is labeled with 0s, 1s and 2s signifying solid phase, pores, and throats respectively. """ xyz = network["pore.coords"] cn = network["throat.conns"] # Distance bounding box from the network by a fixed amount delta = network["pore.diameter"].mean() / 2 if isinstance(network, op.network.Cubic): delta = op.topotools.get_spacing(network).mean() / 2 # Shift everything to avoid out-of-bounds extra_clearance = int(max_dim * 0.05) # Transform points to satisfy origin at (0, 0, 0) xyz0 = xyz.min(axis=0) - delta xyz += -xyz0 res = (xyz.ptp(axis=0).max() + 2 * delta) / max_dim shape = np.rint((xyz.max(axis=0) + delta) / res).astype(int) + 2 * extra_clearance # Transforming from real coords to matrix coords xyz = np.rint(xyz / res).astype(int) + extra_clearance pore_radi = np.rint(network["pore.diameter"] * 0.5 / res).astype(int) throat_radi = np.rint(network["throat.diameter"] * 0.5 / res).astype(int) im_pores = np.zeros(shape, dtype=np.uint8) im_throats = np.zeros_like(im_pores) if pore_shape == "cube": pore_elem = cube rp = pore_radi * 2 + 1 # +1 since num_voxel must be odd rp_max = int(2 * round(delta / res)) + 1 if pore_shape == "sphere": pore_elem = ball rp = pore_radi rp_max = int(round(delta / res)) if throat_shape == "cuboid": raise Exception("Not yet implemented, try 'cylinder'.") # Generating voxels for pores for i, pore in enumerate(tqdm(network.Ps, **settings.tqdm)): elem = pore_elem(rp[i]) try: im_pores = overlay(im1=im_pores, im2=elem, c=xyz[i]) except ValueError: elem = pore_elem(rp_max) im_pores = overlay(im1=im_pores, im2=elem, c=xyz[i]) # Get rid of pore overlaps im_pores[im_pores > 0] = 1 # Generating voxels for throats for i, throat in enumerate(tqdm(network.Ts, **settings.tqdm)): try: im_throats = insert_cylinder( im_throats, r=throat_radi[i], xyz0=xyz[cn[i, 0]], xyz1=xyz[cn[i, 1]]) except ValueError: im_throats = insert_cylinder( im_throats, r=rp_max, xyz0=xyz[cn[i, 0]], xyz1=xyz[cn[i, 1]]) # Get rid of throat overlaps im_throats[im_throats > 0] = 1 # Subtract pore-throat overlap from throats im_throats = (im_throats.astype(bool) * ~im_pores.astype(bool)).astype(np.uint8) im = im_pores * 1 + im_throats * 2 return im[extra_clearance:-extra_clearance, extra_clearance:-extra_clearance, extra_clearance:-extra_clearance]
[docs] def generate_voxel_image(network, pore_shape="sphere", throat_shape="cylinder", max_dim=None, rtol=0.1): r""" Generate a voxel image from an OpenPNM network object Parameters ---------- network : OpenPNM GenericNetwork Network from which voxel image is to be generated pore_shape : str Shape of pores in the network, valid choices are "sphere", "cube" throat_shape : str Shape of throats in the network, valid choices are "cylinder", "cuboid" max_dim : int Number of voxels in the largest dimension of the network rtol : float Stopping criteria for finding the smallest voxel image such that further increasing the number of voxels in each dimension by 25% would improve the predicted porosity of the image by less that ``rtol`` Returns ------- im : ndarray Voxelated image corresponding to the given pore network model Notes ----- (1) The generated voxelated image is labeled with 0s, 1s and 2s signifying solid phase, pores, and throats respectively. (2) If max_dim is not provided, the method calculates it such that the further increasing it doesn't change porosity by much. Examples -------- `Click here <https://porespy.org/examples/networks/reference/generator_voxel_image.html>`_ to view online example. """ logger.info("Generating voxel image from pore network") # If max_dim is provided, generate voxel image using max_dim if max_dim is not None: return _generate_voxel_image( network, pore_shape, throat_shape, max_dim=max_dim) max_dim = 200 # If max_dim is not provided, find best max_dim that predicts porosity err = 100 eps_old = 200 while err > rtol: logger.debug(f"Maximum dimension: {max_dim} voxels") im = _generate_voxel_image( network, pore_shape, throat_shape, max_dim=max_dim) eps = im.astype(bool).sum(dtype=np.int64) / np.prod(im.shape) err = abs(1 - eps / eps_old) eps_old = eps max_dim = int(max_dim * 1.25) logger.debug(f"Converged at max_dim = {max_dim} voxels") return im
[docs] def label_phases( network, alias={1: 'void', 2: 'solid'}): r""" Create pore and throat labels based on 'pore.phase' values Parameters ---------- network : dict The network stored as a dictionary as returned from the ``regions_to_network`` function alias : dict A mapping between integer values in 'pore.phase' and string labels. The default is ``{1: 'void', 2: 'solid'}`` which will result in the labels ``'pore.void'`` and ``'pore.solid'``, as well as ``'throat.solid_void'``, ``'throat.solid_solid'``, and ``'throat.void_void'``. The reverse labels are also added for convenience like ``throat.void_solid``. Returns ------- network : dict The same ``network`` as passed in but with new boolean arrays added for the phase labels. Examples -------- `Click here <https://porespy.org/examples/networks/reference/label_phases.html>`_ to view online example. """ conns = network['throat.conns'] for i in alias.keys(): pore_i_hits = network['pore.phase'] == i network['pore.' + alias[i]] = pore_i_hits for j in alias.keys(): pore_j_hits = network['pore.phase'] == j throat_hits = pore_i_hits[conns[:, 0]] * pore_j_hits[conns[:, 1]] throat_hits += pore_i_hits[conns[:, 1]] * pore_j_hits[conns[:, 0]] if np.any(throat_hits): name = 'throat.' + '_'.join([alias[i], alias[j]]) if name not in network.keys(): network[name] = np.zeros_like(conns[:, 0], dtype=bool) network[name] += throat_hits return network
[docs] def label_boundaries( network, labels=[['left', 'right'], ['front', 'back'], ['top', 'bottom']], tol=1e-9): r""" Create boundary pore labels based on proximity to axis extrema Parameters ---------- network : dict The network stored as a dictionary as returned from the ``regions_to_network`` function labels : list of lists A 3-element list, with each element containing a pair of strings indicating the label to apply to the beginning and end of each axis. The default is ``[['left', 'right'], ['front', 'back'], ['top', 'bottom']]`` which will apply the label ``'left'`` to all pores with the minimum x-coordinate, and ``'right'`` to the pores with the maximum x-coordinate, and so on. Returns ------- network : dict The same ``network`` as passed in but with new boolean arrays added for the boundary labels. Examples -------- `Click here <https://porespy.org/examples/networks/reference/label_boundaries.html>`_ to view online example. """ crds = network['pore.coords'] extents = [[crds[:, i].min(), crds[:, i].max()] for i in range(len(crds[0, :]))] network['pore.boundary'] = np.zeros_like(crds[:, 0], dtype=bool) for i, axis in enumerate(labels): for j, face in enumerate(axis): if face: hits = crds[:, i] == extents[i][j] network['pore.boundary'] += hits network['pore.' + labels[i][j]] = hits return network