Skip to content

Examples

The voids.examples sub-package provides deterministic synthetic networks and images intended for testing, documentation, and reproducible demonstrations.


Demo Networks

voids.examples.demo

make_linear_chain_network

make_linear_chain_network(
    num_pores=3,
    *,
    axis="x",
    length=1.0,
    cross_section=1.0,
    bulk_volume=10.0,
    pore_volume=1.0,
    throat_volume=0.5,
    throat_length=1.0,
    hydraulic_conductance=1.0,
)

Build a deterministic one-dimensional pore-throat chain.

Parameters:

Name Type Description Default
num_pores int

Number of pores in the chain. The number of throats is num_pores - 1.

3
axis str

Axis along which the chain is embedded.

'x'
length float

Sample length along the chosen axis.

1.0
cross_section float

Cross-sectional area normal to the flow axis.

1.0
bulk_volume float

Bulk sample volume associated with the toy problem.

10.0
pore_volume float

Synthetic pore and throat void volumes.

1.0
throat_volume float

Synthetic pore and throat void volumes.

1.0
throat_length float

Length assigned to each throat.

1.0
hydraulic_conductance float

Precomputed throat hydraulic conductance.

1.0

Returns:

Type Description
Network

Synthetic line network with canonical inlet and outlet labels.

Raises:

Type Description
ValueError

If the number of pores, axis, or geometric parameters are invalid.

Notes

The pore coordinates are uniformly spaced so that the pore positions satisfy

x_k = k * length / (num_pores - 1)

along the selected axis. The function is intended for solver smoke tests, tutorials, and regression examples rather than realistic porous-media reconstruction.

Source code in src/voids/examples/demo.py
def make_linear_chain_network(
    num_pores: int = 3,
    *,
    axis: str = "x",
    length: float = 1.0,
    cross_section: float = 1.0,
    bulk_volume: float = 10.0,
    pore_volume: float = 1.0,
    throat_volume: float = 0.5,
    throat_length: float = 1.0,
    hydraulic_conductance: float = 1.0,
) -> Network:
    """Build a deterministic one-dimensional pore-throat chain.

    Parameters
    ----------
    num_pores :
        Number of pores in the chain. The number of throats is
        ``num_pores - 1``.
    axis :
        Axis along which the chain is embedded.
    length :
        Sample length along the chosen axis.
    cross_section :
        Cross-sectional area normal to the flow axis.
    bulk_volume :
        Bulk sample volume associated with the toy problem.
    pore_volume, throat_volume :
        Synthetic pore and throat void volumes.
    throat_length :
        Length assigned to each throat.
    hydraulic_conductance :
        Precomputed throat hydraulic conductance.

    Returns
    -------
    Network
        Synthetic line network with canonical inlet and outlet labels.

    Raises
    ------
    ValueError
        If the number of pores, axis, or geometric parameters are invalid.

    Notes
    -----
    The pore coordinates are uniformly spaced so that the pore positions satisfy

    ``x_k = k * length / (num_pores - 1)``

    along the selected axis. The function is intended for solver smoke tests,
    tutorials, and regression examples rather than realistic porous-media
    reconstruction.
    """

    if num_pores < 2:
        raise ValueError("num_pores must be >= 2")
    if axis not in _AXIS_TO_INDEX:
        raise ValueError("axis must be one of 'x', 'y', or 'z'")
    if length <= 0 or cross_section <= 0 or bulk_volume <= 0:
        raise ValueError("length, cross_section, and bulk_volume must be positive")
    if pore_volume < 0 or throat_volume < 0 or throat_length < 0 or hydraulic_conductance < 0:
        raise ValueError("pore/throat properties must be nonnegative")

    coords = np.zeros((num_pores, 3), dtype=float)
    coords[:, _AXIS_TO_INDEX[axis]] = np.linspace(0.0, float(length), num_pores)
    throat_conns = np.column_stack(
        [np.arange(num_pores - 1, dtype=np.int64), np.arange(1, num_pores, dtype=np.int64)]
    )

    pore_labels: dict[str, np.ndarray] = {
        f"inlet_{axis}min": np.zeros(num_pores, dtype=bool),
        f"outlet_{axis}max": np.zeros(num_pores, dtype=bool),
        "boundary": np.zeros(num_pores, dtype=bool),
    }
    pore_labels[f"inlet_{axis}min"][0] = True
    pore_labels[f"outlet_{axis}max"][-1] = True
    pore_labels["boundary"][[0, -1]] = True

    sample = SampleGeometry(
        bulk_volume=float(bulk_volume),
        lengths={axis: float(length)},
        cross_sections={axis: float(cross_section)},
    )
    provenance = Provenance(
        source_kind="synthetic_demo",
        extraction_method="linear_chain",
        user_notes={"num_pores": int(num_pores), "axis": axis},
    )

    return Network(
        throat_conns=throat_conns,
        pore_coords=coords,
        sample=sample,
        provenance=provenance,
        pore={"volume": np.full(num_pores, float(pore_volume), dtype=float)},
        throat={
            "volume": np.full(num_pores - 1, float(throat_volume), dtype=float),
            "length": np.full(num_pores - 1, float(throat_length), dtype=float),
            "hydraulic_conductance": np.full(
                num_pores - 1, float(hydraulic_conductance), dtype=float
            ),
        },
        pore_labels=pore_labels,
    )

Manufactured Void Images

voids.examples.manufactured

make_manufactured_void_image

make_manufactured_void_image(shape=(48, 48, 48))

Create a deterministic synthetic 3-D void-space image.

Parameters:

Name Type Description Default
shape tuple[int, int, int]

Output image shape in voxels.

(48, 48, 48)

Returns:

Type Description
ndarray

Boolean array with shape shape where True denotes void space.

Notes

The construction is intentionally simple: a chain of overlapping spheres spans the x-direction, while a few side branches create off-axis connectivity. The result is not intended as a geological model. It is a manufactured test image for extraction workflows such as porespy.snow2.

Source code in src/voids/examples/manufactured.py
def make_manufactured_void_image(shape: tuple[int, int, int] = (48, 48, 48)) -> np.ndarray:
    """Create a deterministic synthetic 3-D void-space image.

    Parameters
    ----------
    shape :
        Output image shape in voxels.

    Returns
    -------
    numpy.ndarray
        Boolean array with shape ``shape`` where ``True`` denotes void space.

    Notes
    -----
    The construction is intentionally simple: a chain of overlapping spheres
    spans the x-direction, while a few side branches create off-axis
    connectivity. The result is not intended as a geological model. It is a
    manufactured test image for extraction workflows such as ``porespy.snow2``.
    """

    nx, ny, nz = shape
    X, Y, Z = np.indices(shape)
    im = np.zeros(shape, dtype=bool)

    chain = [
        (6, ny // 2, nz // 2, 7),
        (14, ny // 2 + 1, nz // 2, 7),
        (22, ny // 2 - 1, nz // 2 + 1, 7),
        (30, ny // 2, nz // 2 - 1, 7),
        (38, ny // 2 + 1, nz // 2, 7),
    ]
    branches = [
        (20, ny // 2 + 10, nz // 2, 5),
        (28, ny // 2 - 10, nz // 2 + 2, 5),
        (34, ny // 2 + 6, nz // 2 + 8, 4),
    ]
    for cx, cy, cz, r in chain + branches:
        mask = (X - cx) ** 2 + (Y - cy) ** 2 + (Z - cz) ** 2 <= r**2
        im |= mask

    y0 = ny // 2
    z0 = nz // 2
    im[12:17, y0 - 1 : y0 + 2, z0 - 1 : z0 + 2] = True

    return im

save_default_manufactured_void_image

save_default_manufactured_void_image(path)

Write the manufactured void image to a NumPy .npy file.

Parameters:

Name Type Description Default
path str | Path

Destination file path.

required

Returns:

Type Description
Path

Resolved path that was written.

Source code in src/voids/examples/manufactured.py
def save_default_manufactured_void_image(path: str | Path) -> Path:
    """Write the manufactured void image to a NumPy ``.npy`` file.

    Parameters
    ----------
    path :
        Destination file path.

    Returns
    -------
    pathlib.Path
        Resolved path that was written.
    """

    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    np.save(path, make_manufactured_void_image())
    return path

Mesh Networks

voids.examples.mesh

make_cartesian_mesh_network

make_cartesian_mesh_network(
    shape,
    *,
    spacing=1.0,
    pore_radius=None,
    throat_radius=None,
    thickness=None,
    units=None,
)

Build a regular mesh-like pore network with one pore per mesh node.

Parameters:

Name Type Description Default
shape Sequence[int]

Number of pores along each active axis. Typical examples are (20, 20) and (20, 20, 20).

required
spacing float

Center-to-center pore spacing.

1.0
pore_radius float | None

Synthetic geometric radii used to construct pore and throat attributes.

None
throat_radius float | None

Synthetic geometric radii used to construct pore and throat attributes.

None
thickness float | None

Extrusion thickness for 2-D meshes. Ignored for 3-D meshes.

None
units dict[str, str] | None

Optional unit metadata stored in :class:SampleGeometry.

None

Returns:

Type Description
Network

Synthetic Cartesian lattice network with geometry, labels, and sample metadata.

Raises:

Type Description
ValueError

If the shape, spacing, or geometric radii are invalid.

Notes

Each mesh node becomes one pore, and each nearest-neighbor pair becomes one throat. The resulting graph is a regular square or cubic lattice. For the current synthetic geometry model, the throat core length is

L_core = spacing - 2 * pore_radius

and the throat volume is approximated as

V_throat = A_throat * L_core.

This makes the example useful for solver verification and scaling studies, while remaining intentionally simpler than an image-derived pore network.

Source code in src/voids/examples/mesh.py
def make_cartesian_mesh_network(
    shape: Sequence[int],
    *,
    spacing: float = 1.0,
    pore_radius: float | None = None,
    throat_radius: float | None = None,
    thickness: float | None = None,
    units: dict[str, str] | None = None,
) -> Network:
    """Build a regular mesh-like pore network with one pore per mesh node.

    Parameters
    ----------
    shape :
        Number of pores along each active axis. Typical examples are ``(20, 20)``
        and ``(20, 20, 20)``.
    spacing :
        Center-to-center pore spacing.
    pore_radius, throat_radius :
        Synthetic geometric radii used to construct pore and throat attributes.
    thickness :
        Extrusion thickness for 2-D meshes. Ignored for 3-D meshes.
    units :
        Optional unit metadata stored in :class:`SampleGeometry`.

    Returns
    -------
    Network
        Synthetic Cartesian lattice network with geometry, labels, and sample
        metadata.

    Raises
    ------
    ValueError
        If the shape, spacing, or geometric radii are invalid.

    Notes
    -----
    Each mesh node becomes one pore, and each nearest-neighbor pair becomes one
    throat. The resulting graph is a regular square or cubic lattice. For the
    current synthetic geometry model, the throat core length is

    ``L_core = spacing - 2 * pore_radius``

    and the throat volume is approximated as

    ``V_throat = A_throat * L_core``.

    This makes the example useful for solver verification and scaling studies,
    while remaining intentionally simpler than an image-derived pore network.
    """

    dims = _normalize_shape(shape)
    ndim = len(dims)
    if spacing <= 0:
        raise ValueError("spacing must be positive")

    pore_radius = 0.2 * spacing if pore_radius is None else float(pore_radius)
    throat_radius = 0.1 * spacing if throat_radius is None else float(throat_radius)
    if pore_radius <= 0 or throat_radius <= 0:
        raise ValueError("pore_radius and throat_radius must be positive")
    if pore_radius >= 0.5 * spacing:
        raise ValueError("pore_radius must be smaller than half the pore spacing")
    if throat_radius >= 0.5 * spacing:
        raise ValueError("throat_radius must be smaller than half the pore spacing")

    if ndim == 2:
        nz = 1
        depth = float(spacing if thickness is None else thickness)
        if depth <= 0:
            raise ValueError("thickness must be positive for 2D meshes")
        shape3 = (dims[0], dims[1], nz)
        x = (np.arange(dims[0], dtype=float) + 0.5) * spacing
        y = (np.arange(dims[1], dtype=float) + 0.5) * spacing
        z = np.array([0.5 * depth], dtype=float)
        pore_volume_scalar = np.pi * pore_radius**2 * depth
        cross_sections = {
            "x": dims[1] * spacing * depth,
            "y": dims[0] * spacing * depth,
        }
        lengths = {
            "x": dims[0] * spacing,
            "y": dims[1] * spacing,
        }
        bulk_volume = dims[0] * dims[1] * spacing**2 * depth
    else:
        shape3 = (dims[0], dims[1], dims[2])
        x = (np.arange(dims[0], dtype=float) + 0.5) * spacing
        y = (np.arange(dims[1], dtype=float) + 0.5) * spacing
        z = (np.arange(dims[2], dtype=float) + 0.5) * spacing
        pore_volume_scalar = (4.0 / 3.0) * np.pi * pore_radius**3
        cross_sections = {
            "x": dims[1] * dims[2] * spacing**2,
            "y": dims[0] * dims[2] * spacing**2,
            "z": dims[0] * dims[1] * spacing**2,
        }
        lengths = {
            "x": dims[0] * spacing,
            "y": dims[1] * spacing,
            "z": dims[2] * spacing,
        }
        bulk_volume = dims[0] * dims[1] * dims[2] * spacing**3

    X, Y, Z = np.meshgrid(x, y, z, indexing="ij")
    pore_coords = np.column_stack([X.ravel(), Y.ravel(), Z.ravel()])
    throat_conns = _build_cartesian_connectivity(shape3, ndim=ndim)
    pore_labels = _build_boundary_labels(shape3, ndim=ndim)

    throat_area_scalar = np.pi * throat_radius**2
    throat_perimeter_scalar = 2.0 * np.pi * throat_radius
    throat_core_length_scalar = spacing - 2.0 * pore_radius
    if throat_core_length_scalar <= 0:  # pragma: no cover - guarded by pore_radius < spacing / 2
        raise ValueError(
            "pore_radius is too large relative to spacing; throat core length must stay positive"
        )

    pore_area_scalar = np.pi * pore_radius**2
    pore_perimeter_scalar = 2.0 * np.pi * pore_radius
    n_pores = pore_coords.shape[0]
    n_throats = throat_conns.shape[0]

    pore = {
        "volume": np.full(n_pores, pore_volume_scalar, dtype=float),
        "area": np.full(n_pores, pore_area_scalar, dtype=float),
        "perimeter": np.full(n_pores, pore_perimeter_scalar, dtype=float),
        "shape_factor": np.full(n_pores, _CIRCULAR_SHAPE_FACTOR, dtype=float),
        "radius_inscribed": np.full(n_pores, pore_radius, dtype=float),
        "diameter_inscribed": np.full(n_pores, 2.0 * pore_radius, dtype=float),
    }
    throat = {
        "volume": np.full(n_throats, throat_area_scalar * throat_core_length_scalar, dtype=float),
        "area": np.full(n_throats, throat_area_scalar, dtype=float),
        "perimeter": np.full(n_throats, throat_perimeter_scalar, dtype=float),
        "shape_factor": np.full(n_throats, _CIRCULAR_SHAPE_FACTOR, dtype=float),
        "radius_inscribed": np.full(n_throats, throat_radius, dtype=float),
        "diameter_inscribed": np.full(n_throats, 2.0 * throat_radius, dtype=float),
        "length": np.full(n_throats, spacing, dtype=float),
        "direct_length": np.full(n_throats, spacing, dtype=float),
        "pore1_length": np.full(n_throats, pore_radius, dtype=float),
        "core_length": np.full(n_throats, throat_core_length_scalar, dtype=float),
        "pore2_length": np.full(n_throats, pore_radius, dtype=float),
    }

    sample = SampleGeometry(
        bulk_volume=float(bulk_volume),
        lengths={k: float(v) for k, v in lengths.items()},
        cross_sections={k: float(v) for k, v in cross_sections.items()},
        units=units or {"length": "m", "pressure": "Pa"},
    )
    provenance = Provenance(
        source_kind="synthetic_mesh",
        extraction_method="cartesian_lattice",
        voxel_size_original=float(spacing),
        user_notes={"shape": list(dims)},
    )

    return Network(
        throat_conns=throat_conns,
        pore_coords=pore_coords,
        sample=sample,
        provenance=provenance,
        pore=pore,
        throat=throat,
        pore_labels=pore_labels,
        extra={
            "mesh_shape": tuple(dims),
            "mesh_spacing": float(spacing),
            "mesh_ndim": ndim,
        },
    )