Skip to content

Benchmarks

The voids.benchmarks sub-package provides utilities for cross-checking voids results against reference implementations such as OpenPNM and XLB. In the broader project documentation, these utilities belong to the verification side of the Verification & Validation split: they benchmark voids against software references or alternative numerical workflows, not directly against experimental measurements.

The two high-level segmented-volume benchmark wrappers now share the same physical pressure convention:

  • the preferred public input is the physical pressure drop delta_p, typically in Pa
  • optional pin and pout values can also be supplied when the user wants to preserve a particular absolute pressure reference level
  • for the current incompressible permeability benchmark, only the pressure drop Δp = pin - pout affects the reported permeability
  • the applied p_inlet_physical, p_outlet_physical, and dp_physical values are recorded in the benchmark result tables

So delta_p=1.0, pin=1.0/pout=0.0, and delta_p=1.0 with pin=101326.0/pout=101325.0 all represent the same current benchmark driving condition.

The XLB benchmark API now has two distinct layers:

  • solve_binary_volume_with_xlb is the low-level direct-image solver. It works in lattice units and accepts lattice pressure boundary conditions through pressure_inlet_lattice, pressure_outlet_lattice, or pressure_drop_lattice.
  • benchmark_segmented_volume_with_xlb is the high-level verification wrapper. It resolves a physical pressure drop from delta_p and optional pin / pout, then maps that same physical Δp into lattice units before calling XLB on the original binary image.

For the high-level XLB benchmark, fluid.density must be provided because the shared physical pressure drop must be converted into lattice pressure units.


Cross-Check

voids.benchmarks.crosscheck

SinglePhaseCrosscheckSummary dataclass

Summary of a solver-to-reference comparison.

Attributes:

Name Type Description
reference str

Name of the reference implementation or workflow.

axis str

Flow axis used in the comparison.

permeability_abs_diff, permeability_rel_diff

Absolute and relative differences between apparent permeabilities.

total_flow_abs_diff, total_flow_rel_diff

Absolute and relative differences between total flow rates.

details dict[str, Any]

Auxiliary metadata useful for debugging and reporting.

Source code in src/voids/benchmarks/crosscheck.py
@dataclass(slots=True)
class SinglePhaseCrosscheckSummary:
    """Summary of a solver-to-reference comparison.

    Attributes
    ----------
    reference :
        Name of the reference implementation or workflow.
    axis :
        Flow axis used in the comparison.
    permeability_abs_diff, permeability_rel_diff :
        Absolute and relative differences between apparent permeabilities.
    total_flow_abs_diff, total_flow_rel_diff :
        Absolute and relative differences between total flow rates.
    details :
        Auxiliary metadata useful for debugging and reporting.
    """

    reference: str
    axis: str
    permeability_abs_diff: float
    permeability_rel_diff: float
    total_flow_abs_diff: float
    total_flow_rel_diff: float
    details: dict[str, Any]

crosscheck_singlephase_roundtrip_openpnm_dict

crosscheck_singlephase_roundtrip_openpnm_dict(
    net, fluid, bc, *, axis, options=None
)

Cross-check voids after a dict roundtrip through OpenPNM-style keys.

Parameters:

Name Type Description Default
net Network

Network to solve and round-trip.

required
fluid FluidSinglePhase

Fluid properties.

required
bc PressureBC

Pressure boundary conditions.

required
axis str

Flow axis used in the permeability calculation.

required
options SinglePhaseOptions | None

Optional solver configuration.

None

Returns:

Type Description
SinglePhaseCrosscheckSummary

Comparison between the original voids solve and the round-tripped solve.

Notes

This path does not require OpenPNM itself. It checks whether exporting to the flat OpenPNM/PoreSpy naming convention and importing back into voids changes any transport-relevant fields.

Source code in src/voids/benchmarks/crosscheck.py
def crosscheck_singlephase_roundtrip_openpnm_dict(
    net: Network,
    fluid: FluidSinglePhase,
    bc: PressureBC,
    *,
    axis: str,
    options: SinglePhaseOptions | None = None,
) -> SinglePhaseCrosscheckSummary:
    """Cross-check ``voids`` after a dict roundtrip through OpenPNM-style keys.

    Parameters
    ----------
    net :
        Network to solve and round-trip.
    fluid :
        Fluid properties.
    bc :
        Pressure boundary conditions.
    axis :
        Flow axis used in the permeability calculation.
    options :
        Optional solver configuration.

    Returns
    -------
    SinglePhaseCrosscheckSummary
        Comparison between the original ``voids`` solve and the round-tripped solve.

    Notes
    -----
    This path does not require OpenPNM itself. It checks whether exporting to the
    flat OpenPNM/PoreSpy naming convention and importing back into ``voids`` changes
    any transport-relevant fields.
    """

    options = options or SinglePhaseOptions()
    r0 = solve(net, fluid=fluid, bc=bc, axis=axis, options=options)
    op_dict = to_openpnm_dict(net)
    net_rt = from_porespy(op_dict, sample=net.sample, provenance=net.provenance)
    r1 = solve(net_rt, fluid=fluid, bc=bc, axis=axis, options=options)
    return _summary_from_results("openpnm_dict_roundtrip", axis, r0, r1)

crosscheck_singlephase_with_openpnm

crosscheck_singlephase_with_openpnm(
    net, fluid, bc, *, axis, options=None
)

Cross-check voids against OpenPNM StokesFlow.

Parameters:

Name Type Description Default
net Network

Network to simulate.

required
fluid FluidSinglePhase

Fluid properties.

required
bc PressureBC

Pressure boundary conditions.

required
axis str

Flow axis used for apparent permeability.

required
options SinglePhaseOptions | None

Optional solver configuration.

None

Returns:

Type Description
SinglePhaseCrosscheckSummary

Comparison between voids and OpenPNM.

Raises:

Type Description
ImportError

If OpenPNM is not installed.

RuntimeError

If the installed OpenPNM API is incompatible with the adapter.

ValueError

If the imposed pressure drop is zero.

Notes

The comparison injects the voids-computed throat.hydraulic_conductance into OpenPNM. That means the crosscheck isolates differences in system assembly, boundary-condition handling, sign conventions, and linear-solver behavior, rather than differences in geometric conductance modeling.

Source code in src/voids/benchmarks/crosscheck.py
def crosscheck_singlephase_with_openpnm(
    net: Network,
    fluid: FluidSinglePhase,
    bc: PressureBC,
    *,
    axis: str,
    options: SinglePhaseOptions | None = None,
) -> SinglePhaseCrosscheckSummary:
    """Cross-check ``voids`` against OpenPNM StokesFlow.

    Parameters
    ----------
    net :
        Network to simulate.
    fluid :
        Fluid properties.
    bc :
        Pressure boundary conditions.
    axis :
        Flow axis used for apparent permeability.
    options :
        Optional solver configuration.

    Returns
    -------
    SinglePhaseCrosscheckSummary
        Comparison between ``voids`` and OpenPNM.

    Raises
    ------
    ImportError
        If OpenPNM is not installed.
    RuntimeError
        If the installed OpenPNM API is incompatible with the adapter.
    ValueError
        If the imposed pressure drop is zero.

    Notes
    -----
    The comparison injects the ``voids``-computed ``throat.hydraulic_conductance``
    into OpenPNM. That means the crosscheck isolates differences in system assembly,
    boundary-condition handling, sign conventions, and linear-solver behavior,
    rather than differences in geometric conductance modeling.
    """

    try:
        import openpnm as op
    except Exception as exc:  # pragma: no cover - depends on optional env
        raise ImportError(
            "OpenPNM is not installed. Use the 'test' pixi environment or install openpnm."
        ) from exc

    options = options or SinglePhaseOptions()
    r_voids = solve(net, fluid=fluid, bc=bc, axis=axis, options=options)
    g = np.asarray(r_voids.throat_conductance, dtype=float)

    pn = to_openpnm_network(net, copy_properties=False, copy_labels=True)
    phase = _openpnm_phase_factory(op, pn)
    phase["throat.hydraulic_conductance"] = g

    sf = op.algorithms.StokesFlow(network=pn, phase=phase)
    inlet_mask = np.asarray(net.pore_labels[bc.inlet_label], dtype=bool)
    outlet_mask = np.asarray(net.pore_labels[bc.outlet_label], dtype=bool)
    inlet = np.where(inlet_mask)[0]
    outlet = np.where(outlet_mask)[0]

    if hasattr(sf, "set_value_BC"):
        sf.set_value_BC(pores=inlet, values=float(bc.pin))
        sf.set_value_BC(pores=outlet, values=float(bc.pout))
    elif hasattr(sf, "set_BC"):
        sf.set_BC(pores=inlet, bctype="value", bcvalues=float(bc.pin))
        sf.set_BC(pores=outlet, bctype="value", bcvalues=float(bc.pout))
    else:  # pragma: no cover
        raise RuntimeError("OpenPNM StokesFlow object does not expose a recognizable BC API")

    sf.run()
    p_ref = _get_openpnm_pressure(sf)

    q_rate = np.asarray(sf.rate(pores=inlet), dtype=float)
    q_ref_raw = float(q_rate.sum())
    q_ref = q_ref_raw
    if np.isfinite(q_ref) and np.isfinite(r_voids.total_flow_rate):
        if np.isclose(abs(q_ref), abs(r_voids.total_flow_rate), rtol=1e-8, atol=1e-14):
            q_ref = float(np.copysign(abs(q_ref), r_voids.total_flow_rate))

    dP = float(bc.pin - bc.pout)
    if abs(dP) == 0.0:
        raise ValueError("Pressure drop pin-pout must be nonzero")
    L = net.sample.length_for_axis(axis)
    Axs = net.sample.area_for_axis(axis)
    mu_ref = fluid.reference_viscosity(pin=bc.pin, pout=bc.pout)
    k_ref = abs(q_ref_raw) * mu_ref * L / (Axs * abs(dP))
    k_voids = float((r_voids.permeability or {}).get(axis, np.nan))

    return _summary_from_values(
        reference="openpnm_stokesflow",
        axis=axis,
        k_voids=k_voids,
        k_ref=float(k_ref),
        q_voids=float(r_voids.total_flow_rate),
        q_ref=float(q_ref),
        details={
            "openpnm_version": getattr(op, "__version__", "unknown"),
            "q_ref_raw": q_ref_raw,
            "n_inlet_pores": int(inlet.size),
            "n_outlet_pores": int(outlet.size),
            "conductance_model": options.conductance_model,
            "solver_voids": options.solver,
            "p_ref_min": float(np.min(p_ref)) if p_ref.size else np.nan,
            "p_ref_max": float(np.max(p_ref)) if p_ref.size else np.nan,
        },
    )

Segmented Volume Benchmarks

voids.benchmarks.segmented_volume

SegmentedVolumeCrosscheckResult dataclass

Store extraction, porosity, and solver cross-check outputs.

Attributes:

Name Type Description
extract NetworkExtractionResult

Result of importing the segmented volume into a voids network and pruning it to the requested spanning axis.

fluid FluidSinglePhase

Fluid properties used in the permeability solve.

bc PressureBC

Pressure boundary conditions imposed on the extracted network.

options SinglePhaseOptions

Solver and conductance options used for the comparison.

image_porosity float

Void fraction of the segmented binary image.

absolute_porosity, effective_porosity

Porosity diagnostics computed from the pruned extracted network.

summary SinglePhaseCrosscheckSummary

Comparison summary between voids and OpenPNM.

Notes

This high-level benchmark follows the same public pressure-BC convention as benchmark_segmented_volume_with_xlb: the preferred user input is delta_p, while optional pin / pout values can still be used to preserve an absolute pressure gauge. The applied physical pressures and pressure drop are recorded explicitly in :meth:SegmentedVolumeCrosscheckResult.to_record.

Source code in src/voids/benchmarks/segmented_volume.py
@dataclass(slots=True)
class SegmentedVolumeCrosscheckResult:
    """Store extraction, porosity, and solver cross-check outputs.

    Attributes
    ----------
    extract :
        Result of importing the segmented volume into a `voids` network and
        pruning it to the requested spanning axis.
    fluid :
        Fluid properties used in the permeability solve.
    bc :
        Pressure boundary conditions imposed on the extracted network.
    options :
        Solver and conductance options used for the comparison.
    image_porosity :
        Void fraction of the segmented binary image.
    absolute_porosity, effective_porosity :
        Porosity diagnostics computed from the pruned extracted network.
    summary :
        Comparison summary between `voids` and OpenPNM.

    Notes
    -----
    This high-level benchmark follows the same public pressure-BC convention as
    `benchmark_segmented_volume_with_xlb`: the preferred user input is
    ``delta_p``, while optional ``pin`` / ``pout`` values can still be used to
    preserve an absolute pressure gauge. The applied physical pressures and
    pressure drop are recorded explicitly in
    :meth:`SegmentedVolumeCrosscheckResult.to_record`.
    """

    extract: NetworkExtractionResult
    fluid: FluidSinglePhase
    bc: PressureBC
    options: SinglePhaseOptions
    image_porosity: float
    absolute_porosity: float
    effective_porosity: float
    summary: SinglePhaseCrosscheckSummary

    def to_record(self) -> dict[str, Any]:
        """Return scalar diagnostics suitable for tabulation."""

        details = dict(self.summary.details)
        return {
            "flow_axis": self.summary.axis,
            "phi_image": float(self.image_porosity),
            "phi_abs": float(self.absolute_porosity),
            "phi_eff": float(self.effective_porosity),
            "Np": int(self.extract.net.Np),
            "Nt": int(self.extract.net.Nt),
            "k_voids": float(details["k_voids"]),
            "k_openpnm": float(details["k_ref"]),
            "k_abs_diff": float(self.summary.permeability_abs_diff),
            "k_rel_diff": float(self.summary.permeability_rel_diff),
            "Q_voids": float(details["Q_voids"]),
            "Q_openpnm": float(details["Q_ref"]),
            "Q_abs_diff": float(self.summary.total_flow_abs_diff),
            "Q_rel_diff": float(self.summary.total_flow_rel_diff),
            "n_inlet_pores": int(details["n_inlet_pores"]),
            "n_outlet_pores": int(details["n_outlet_pores"]),
            "conductance_model": str(
                details.get("conductance_model", self.options.conductance_model)
            ),
            "solver_voids": str(details.get("solver_voids", self.options.solver)),
            "p_inlet_physical": float(self.bc.pin),
            "p_outlet_physical": float(self.bc.pout),
            "dp_physical": float(self.bc.pin - self.bc.pout),
            "backend": str(self.extract.backend),
            "backend_version": self.extract.backend_version,
            "openpnm_version": details.get("openpnm_version"),
        }

to_record

to_record()

Return scalar diagnostics suitable for tabulation.

Source code in src/voids/benchmarks/segmented_volume.py
def to_record(self) -> dict[str, Any]:
    """Return scalar diagnostics suitable for tabulation."""

    details = dict(self.summary.details)
    return {
        "flow_axis": self.summary.axis,
        "phi_image": float(self.image_porosity),
        "phi_abs": float(self.absolute_porosity),
        "phi_eff": float(self.effective_porosity),
        "Np": int(self.extract.net.Np),
        "Nt": int(self.extract.net.Nt),
        "k_voids": float(details["k_voids"]),
        "k_openpnm": float(details["k_ref"]),
        "k_abs_diff": float(self.summary.permeability_abs_diff),
        "k_rel_diff": float(self.summary.permeability_rel_diff),
        "Q_voids": float(details["Q_voids"]),
        "Q_openpnm": float(details["Q_ref"]),
        "Q_abs_diff": float(self.summary.total_flow_abs_diff),
        "Q_rel_diff": float(self.summary.total_flow_rel_diff),
        "n_inlet_pores": int(details["n_inlet_pores"]),
        "n_outlet_pores": int(details["n_outlet_pores"]),
        "conductance_model": str(
            details.get("conductance_model", self.options.conductance_model)
        ),
        "solver_voids": str(details.get("solver_voids", self.options.solver)),
        "p_inlet_physical": float(self.bc.pin),
        "p_outlet_physical": float(self.bc.pout),
        "dp_physical": float(self.bc.pin - self.bc.pout),
        "backend": str(self.extract.backend),
        "backend_version": self.extract.backend_version,
        "openpnm_version": details.get("openpnm_version"),
    }

benchmark_segmented_volume_with_openpnm

benchmark_segmented_volume_with_openpnm(
    phases,
    *,
    voxel_size,
    flow_axis=None,
    fluid=None,
    delta_p=None,
    pin=None,
    pout=None,
    options=None,
    length_unit="m",
    pressure_unit="Pa",
    extraction_kwargs=None,
    provenance_notes=None,
    strict=True,
)

Benchmark an extracted segmented volume against OpenPNM.

Parameters:

Name Type Description Default
phases ndarray

Binary segmented image encoded as void=1 and solid=0.

required
voxel_size float

Edge length of one voxel in the declared length unit.

required
flow_axis str | None

Requested transport axis. When omitted, the longest image axis is used.

None
fluid FluidSinglePhase | None

Fluid properties. Defaults to water-like viscosity 1e-3 Pa s.

None
delta_p float | None

Preferred physical pressure drop for the benchmark, typically in Pa. When provided alone, the wrapper uses pout = 0 and pin = delta_p as a gauge choice. When combined with one of pin or pout, the missing value is inferred. When combined with both, consistency with pin - pout is enforced.

None
pin float | None

Optional absolute physical inlet and outlet pressures. They are kept for backward compatibility and for cases where the user wants to preserve a particular pressure reference level. For the current incompressible benchmark, only the pressure drop pin - pout affects the reported permeability.

None
pout float | None

Optional absolute physical inlet and outlet pressures. They are kept for backward compatibility and for cases where the user wants to preserve a particular pressure reference level. For the current incompressible benchmark, only the pressure drop pin - pout affects the reported permeability.

None
options SinglePhaseOptions | None

Solver controls. Defaults to the image-workflow baseline valvatne_blunt with the direct linear solver.

None
length_unit str

Units attached to the extracted sample geometry.

'm'
pressure_unit str

Units attached to the extracted sample geometry.

'm'
extraction_kwargs dict[str, object] | None

Extra keyword arguments forwarded to porespy.networks.snow2.

None
provenance_notes dict[str, object] | None

Optional metadata attached to the extracted network provenance.

None
strict bool

Forwarded to :func:voids.image.extract_spanning_pore_network.

True

Returns:

Type Description
SegmentedVolumeCrosscheckResult

Extraction metadata, porosity diagnostics, and the OpenPNM comparison.

Raises:

Type Description
ValueError

If the image is invalid, the pressure specification is inconsistent, or the implied pressure drop is not positive.

Notes

This helper uses :func:voids.benchmarks.crosscheck_singlephase_with_openpnm, which injects the voids throat hydraulic conductances into OpenPNM. The resulting comparison isolates extraction consistency, boundary-condition handling, and linear-solver agreement; it does not benchmark independent conductance models between packages.

Unlike the XLB high-level benchmark, no fluid-density-based unit conversion is needed here because both sides solve the same extracted pore network directly under the same physical pressure BC.

The absolute pressure offset is numerically immaterial for this current incompressible benchmark. For example, delta_p=1, pin=1/pout=0, and delta_p=1 with pin=101326/pout=101325 all impose the same permeability-driving pressure drop.

Source code in src/voids/benchmarks/segmented_volume.py
def benchmark_segmented_volume_with_openpnm(
    phases: np.ndarray,
    *,
    voxel_size: float,
    flow_axis: str | None = None,
    fluid: FluidSinglePhase | None = None,
    delta_p: float | None = None,
    pin: float | None = None,
    pout: float | None = None,
    options: SinglePhaseOptions | None = None,
    length_unit: str = "m",
    pressure_unit: str = "Pa",
    extraction_kwargs: dict[str, object] | None = None,
    provenance_notes: dict[str, object] | None = None,
    strict: bool = True,
) -> SegmentedVolumeCrosscheckResult:
    """Benchmark an extracted segmented volume against OpenPNM.

    Parameters
    ----------
    phases :
        Binary segmented image encoded as ``void=1`` and ``solid=0``.
    voxel_size :
        Edge length of one voxel in the declared length unit.
    flow_axis :
        Requested transport axis. When omitted, the longest image axis is used.
    fluid :
        Fluid properties. Defaults to water-like viscosity `1e-3 Pa s`.
    delta_p :
        Preferred physical pressure drop for the benchmark, typically in Pa.
        When provided alone, the wrapper uses `pout = 0` and `pin = delta_p` as
        a gauge choice. When combined with one of `pin` or `pout`, the missing
        value is inferred. When combined with both, consistency with
        ``pin - pout`` is enforced.
    pin, pout :
        Optional absolute physical inlet and outlet pressures. They are kept for
        backward compatibility and for cases where the user wants to preserve a
        particular pressure reference level. For the current incompressible
        benchmark, only the pressure drop ``pin - pout`` affects the reported
        permeability.
    options :
        Solver controls. Defaults to the image-workflow baseline
        ``valvatne_blunt`` with the direct linear solver.
    length_unit, pressure_unit :
        Units attached to the extracted sample geometry.
    extraction_kwargs :
        Extra keyword arguments forwarded to `porespy.networks.snow2`.
    provenance_notes :
        Optional metadata attached to the extracted network provenance.
    strict :
        Forwarded to :func:`voids.image.extract_spanning_pore_network`.

    Returns
    -------
    SegmentedVolumeCrosscheckResult
        Extraction metadata, porosity diagnostics, and the OpenPNM comparison.

    Raises
    ------
    ValueError
        If the image is invalid, the pressure specification is inconsistent, or
        the implied pressure drop is not positive.

    Notes
    -----
    This helper uses :func:`voids.benchmarks.crosscheck_singlephase_with_openpnm`,
    which injects the `voids` throat hydraulic conductances into OpenPNM. The
    resulting comparison isolates extraction consistency, boundary-condition
    handling, and linear-solver agreement; it does not benchmark independent
    conductance models between packages.

    Unlike the XLB high-level benchmark, no fluid-density-based unit conversion
    is needed here because both sides solve the same extracted pore network
    directly under the same physical pressure BC.

    The absolute pressure offset is numerically immaterial for this current
    incompressible benchmark. For example, ``delta_p=1``, ``pin=1``/``pout=0``,
    and ``delta_p=1`` with ``pin=101326``/``pout=101325`` all impose the same
    permeability-driving pressure drop.
    """

    arr = _as_binary_volume(phases)
    image_phi = float(arr.mean())
    pin_used, pout_used, _ = resolve_benchmark_pressures(
        delta_p=delta_p,
        pin=pin,
        pout=pout,
    )

    notes = dict(provenance_notes or {})
    notes.setdefault("benchmark_kind", "segmented_volume_openpnm")

    extract = extract_spanning_pore_network(
        arr,
        voxel_size=voxel_size,
        flow_axis=flow_axis,
        length_unit=length_unit,
        pressure_unit=pressure_unit,
        extraction_kwargs=extraction_kwargs,
        provenance_notes=notes,
        strict=strict,
    )

    fluid_used = fluid or FluidSinglePhase(viscosity=1.0e-3)
    options_used = options or SinglePhaseOptions(
        conductance_model="valvatne_blunt",
        solver="direct",
    )
    axis = extract.flow_axis
    bc = make_benchmark_pressure_bc(axis, pin=pin_used, pout=pout_used)
    summary = crosscheck_singlephase_with_openpnm(
        extract.net,
        fluid=fluid_used,
        bc=bc,
        axis=axis,
        options=options_used,
    )

    return SegmentedVolumeCrosscheckResult(
        extract=extract,
        fluid=fluid_used,
        bc=bc,
        options=options_used,
        image_porosity=image_phi,
        absolute_porosity=float(absolute_porosity(extract.net)),
        effective_porosity=float(effective_porosity(extract.net, axis=axis)),
        summary=summary,
    )

voids.benchmarks.xlb

Direct-image permeability benchmarks against XLB.

This module exposes two public layers:

  • solve_binary_volume_with_xlb for a direct-image XLB solve configured in lattice units, with pressure BCs specified through lattice pressure values or a lattice pressure drop.
  • benchmark_segmented_volume_with_xlb for the scientifically relevant voids versus XLB comparison on the same segmented image. This high-level wrapper resolves a physical pressure drop from delta_p and optional pin / pout inputs, then maps that same physical pressure drop into lattice pressure units for XLB.

XLBOptions dataclass

Numerical controls for the direct-image XLB benchmark.

Attributes:

Name Type Description
formulation str

Either "incompressible_navier_stokes" or "steady_stokes_limit".

backend str

XLB compute backend. The current voids adapter supports only "jax".

precision_policy str

XLB precision policy name, for example "FP32FP32".

collision_model str

XLB collision operator label passed to the stepper.

streaming_scheme str

XLB streaming scheme label passed to the stepper.

lattice_viscosity float

Kinematic viscosity in lattice units.

pressure_inlet_lattice, pressure_outlet_lattice

Optional inlet and outlet lattice pressures. If both are provided they define the pressure BC directly.

pressure_drop_lattice float | None

Optional lattice pressure drop. When set without explicit inlet/outlet pressures, it is applied relative to reference_density_lattice.

reference_density_lattice float

Reference lattice density used to construct a baseline outlet pressure when only pressure_drop_lattice is provided.

rho_inlet, rho_outlet

Legacy density-based BC inputs retained for backward compatibility. They are converted internally to lattice pressure using p_lu = c_s^2 rho.

inlet_outlet_buffer_cells int

Number of fluid buffer layers inserted ahead of and behind the sample.

max_steps, min_steps, check_interval, steady_rtol

Iteration and convergence controls for the steady-state solve.

Notes

The current voids adapter uses XLB's JAX backend only. This keeps the dependency path compatible with CPU-only macOS and Linux environments, which is the relevant portability target for the benchmark notebook.

The currently exposed XLB operator is the incompressible Navier-Stokes lattice-Boltzmann stepper. Setting formulation="steady_stokes_limit" does not switch to a different PDE solver; it selects conservative forcing and convergence defaults so the converged solution can be interpreted in the steady creeping-flow limit.

In this isothermal LBM setting, lattice pressure satisfies p_lu = c_s^2 rho. The preferred public inputs are therefore the lattice pressure fields pressure_inlet_lattice / pressure_outlet_lattice or the pressure drop pressure_drop_lattice. The legacy fields rho_inlet and rho_outlet remain supported for backward compatibility and are converted internally to pressure.

Source code in src/voids/benchmarks/xlb.py
@dataclass(slots=True)
class XLBOptions:
    """Numerical controls for the direct-image XLB benchmark.

    Attributes
    ----------
    formulation :
        Either ``"incompressible_navier_stokes"`` or
        ``"steady_stokes_limit"``.
    backend :
        XLB compute backend. The current `voids` adapter supports only
        ``"jax"``.
    precision_policy :
        XLB precision policy name, for example ``"FP32FP32"``.
    collision_model :
        XLB collision operator label passed to the stepper.
    streaming_scheme :
        XLB streaming scheme label passed to the stepper.
    lattice_viscosity :
        Kinematic viscosity in lattice units.
    pressure_inlet_lattice, pressure_outlet_lattice :
        Optional inlet and outlet lattice pressures. If both are provided they
        define the pressure BC directly.
    pressure_drop_lattice :
        Optional lattice pressure drop. When set without explicit inlet/outlet
        pressures, it is applied relative to ``reference_density_lattice``.
    reference_density_lattice :
        Reference lattice density used to construct a baseline outlet pressure
        when only ``pressure_drop_lattice`` is provided.
    rho_inlet, rho_outlet :
        Legacy density-based BC inputs retained for backward compatibility.
        They are converted internally to lattice pressure using
        ``p_lu = c_s^2 rho``.
    inlet_outlet_buffer_cells :
        Number of fluid buffer layers inserted ahead of and behind the sample.
    max_steps, min_steps, check_interval, steady_rtol :
        Iteration and convergence controls for the steady-state solve.

    Notes
    -----
    The current `voids` adapter uses XLB's JAX backend only. This keeps the
    dependency path compatible with CPU-only macOS and Linux environments, which
    is the relevant portability target for the benchmark notebook.

    The currently exposed XLB operator is the incompressible Navier-Stokes
    lattice-Boltzmann stepper. Setting ``formulation="steady_stokes_limit"``
    does not switch to a different PDE solver; it selects conservative forcing
    and convergence defaults so the converged solution can be interpreted in the
    steady creeping-flow limit.

    In this isothermal LBM setting, lattice pressure satisfies
    ``p_lu = c_s^2 rho``. The preferred public inputs are therefore the lattice
    pressure fields ``pressure_inlet_lattice`` / ``pressure_outlet_lattice`` or
    the pressure drop ``pressure_drop_lattice``. The legacy fields ``rho_inlet``
    and ``rho_outlet`` remain supported for backward compatibility and are
    converted internally to pressure.
    """

    formulation: str = "incompressible_navier_stokes"
    backend: str = "jax"
    precision_policy: str = "FP32FP32"
    collision_model: str = "BGK"
    streaming_scheme: str = "pull"
    lattice_viscosity: float = 0.10
    pressure_inlet_lattice: float | None = None
    pressure_outlet_lattice: float | None = None
    pressure_drop_lattice: float | None = DEFAULT_PRESSURE_DROP_LATTICE
    reference_density_lattice: float = DEFAULT_REFERENCE_DENSITY_LATTICE
    rho_inlet: float | None = None
    rho_outlet: float | None = None
    inlet_outlet_buffer_cells: int = 6
    max_steps: int = 2000
    min_steps: int = 200
    check_interval: int = 100
    steady_rtol: float = 1.0e-3

    @classmethod
    def steady_stokes_defaults(cls, **overrides: float | int | str) -> "XLBOptions":
        """Return a conservative preset for the steady creeping-flow limit.

        Notes
        -----
        XLB does not currently expose a separate Stokes-only stepper in the
        installed package used by `voids`. This preset therefore still uses the
        incompressible Navier-Stokes LBM operator, but with a smaller lattice
        pressure drop and tighter steady-state controls so the converged solution is
        interpreted in the low-Reynolds, low-Mach limit.
        """

        values: dict[str, Any] = {
            "formulation": "steady_stokes_limit",
            "lattice_viscosity": 0.10,
            "pressure_drop_lattice": DEFAULT_STOKES_PRESSURE_DROP_LATTICE,
            "inlet_outlet_buffer_cells": 6,
            "max_steps": 4000,
            "min_steps": 800,
            "check_interval": 100,
            "steady_rtol": 5.0e-4,
        }
        values.update(overrides)
        return cls(**values)

steady_stokes_defaults classmethod

steady_stokes_defaults(**overrides)

Return a conservative preset for the steady creeping-flow limit.

Notes

XLB does not currently expose a separate Stokes-only stepper in the installed package used by voids. This preset therefore still uses the incompressible Navier-Stokes LBM operator, but with a smaller lattice pressure drop and tighter steady-state controls so the converged solution is interpreted in the low-Reynolds, low-Mach limit.

Source code in src/voids/benchmarks/xlb.py
@classmethod
def steady_stokes_defaults(cls, **overrides: float | int | str) -> "XLBOptions":
    """Return a conservative preset for the steady creeping-flow limit.

    Notes
    -----
    XLB does not currently expose a separate Stokes-only stepper in the
    installed package used by `voids`. This preset therefore still uses the
    incompressible Navier-Stokes LBM operator, but with a smaller lattice
    pressure drop and tighter steady-state controls so the converged solution is
    interpreted in the low-Reynolds, low-Mach limit.
    """

    values: dict[str, Any] = {
        "formulation": "steady_stokes_limit",
        "lattice_viscosity": 0.10,
        "pressure_drop_lattice": DEFAULT_STOKES_PRESSURE_DROP_LATTICE,
        "inlet_outlet_buffer_cells": 6,
        "max_steps": 4000,
        "min_steps": 800,
        "check_interval": 100,
        "steady_rtol": 5.0e-4,
    }
    values.update(overrides)
    return cls(**values)

XLBDirectSimulationResult dataclass

Store direct-image LBM outputs from an XLB run.

Attributes:

Name Type Description
lattice_pressure_inlet, lattice_pressure_outlet, lattice_pressure_drop

Resolved inlet, outlet, and differential pressure in lattice units.

lattice_density_inlet, lattice_density_outlet

Equivalent lattice densities associated with the pressure BCs through p_lu = c_s^2 rho.

permeability float

Apparent permeability mapped back to physical units.

max_mach_lattice, reynolds_voxel_max

Low-inertia diagnostics useful when interpreting a run as a creeping-flow reference.

Source code in src/voids/benchmarks/xlb.py
@dataclass(slots=True)
class XLBDirectSimulationResult:
    """Store direct-image LBM outputs from an XLB run.

    Attributes
    ----------
    lattice_pressure_inlet, lattice_pressure_outlet, lattice_pressure_drop :
        Resolved inlet, outlet, and differential pressure in lattice units.
    lattice_density_inlet, lattice_density_outlet :
        Equivalent lattice densities associated with the pressure BCs through
        ``p_lu = c_s^2 rho``.
    permeability :
        Apparent permeability mapped back to physical units.
    max_mach_lattice, reynolds_voxel_max :
        Low-inertia diagnostics useful when interpreting a run as a creeping-flow
        reference.
    """

    flow_axis: str
    voxel_size: float
    image_porosity: float
    sample_lengths: dict[str, float]
    sample_cross_sections: dict[str, float]
    lattice_viscosity: float
    lattice_pressure_inlet: float
    lattice_pressure_outlet: float
    lattice_density_inlet: float
    lattice_density_outlet: float
    lattice_pressure_drop: float
    inlet_outlet_buffer_cells: int
    omega: float
    superficial_velocity_lattice: float
    superficial_velocity_profile_lattice: np.ndarray
    velocity_lattice: np.ndarray
    axial_velocity_lattice: np.ndarray
    converged: bool
    n_steps: int
    convergence_metric: float
    permeability: float
    backend: str
    backend_version: str | None
    formulation: str
    velocity_set: str
    collision_model: str
    streaming_scheme: str
    max_speed_lattice: float
    max_mach_lattice: float
    reynolds_voxel_max: float

SegmentedVolumeXLBResult dataclass

Store extraction, porosity, and direct-image XLB benchmark outputs.

Attributes:

Name Type Description
bc PressureBC

Physical pressure BC used on the extracted-network voids solve.

xlb_options XLBOptions

XLB options actually used for the direct-image solve. For the high-level benchmark wrapper these are pressure-coupled so they match the resolved physical pressure drop used on the voids side.

xlb_result XLBDirectSimulationResult

Direct-image XLB result, including resolved lattice pressure diagnostics.

Source code in src/voids/benchmarks/xlb.py
@dataclass(slots=True)
class SegmentedVolumeXLBResult:
    """Store extraction, porosity, and direct-image XLB benchmark outputs.

    Attributes
    ----------
    bc :
        Physical pressure BC used on the extracted-network `voids` solve.
    xlb_options :
        XLB options actually used for the direct-image solve. For the high-level
        benchmark wrapper these are pressure-coupled so they match the resolved
        physical pressure drop used on the `voids` side.
    xlb_result :
        Direct-image XLB result, including resolved lattice pressure diagnostics.
    """

    extract: NetworkExtractionResult
    fluid: FluidSinglePhase
    bc: PressureBC
    options: SinglePhaseOptions
    xlb_options: XLBOptions
    image_porosity: float
    absolute_porosity: float
    effective_porosity: float
    voids_result: SinglePhaseResult
    xlb_result: XLBDirectSimulationResult
    permeability_abs_diff: float
    permeability_rel_diff: float

    def to_record(self) -> dict[str, float | int | str | bool | None]:
        """Return scalar diagnostics suitable for tabulation."""

        k_voids = float((self.voids_result.permeability or {}).get(self.extract.flow_axis, np.nan))
        return {
            "flow_axis": self.extract.flow_axis,
            "phi_image": float(self.image_porosity),
            "phi_abs": float(self.absolute_porosity),
            "phi_eff": float(self.effective_porosity),
            "Np": int(self.extract.net.Np),
            "Nt": int(self.extract.net.Nt),
            "k_voids": k_voids,
            "k_xlb": float(self.xlb_result.permeability),
            "k_abs_diff": float(self.permeability_abs_diff),
            "k_rel_diff": float(self.permeability_rel_diff),
            "voids_mass_balance_error": float(self.voids_result.mass_balance_error),
            "conductance_model": str(self.options.conductance_model),
            "solver_voids": str(self.options.solver),
            "p_inlet_physical": float(self.bc.pin),
            "p_outlet_physical": float(self.bc.pout),
            "dp_physical": float(self.bc.pin - self.bc.pout),
            "extract_backend": str(self.extract.backend),
            "extract_backend_version": self.extract.backend_version,
            "xlb_backend": str(self.xlb_result.backend),
            "xlb_backend_version": self.xlb_result.backend_version,
            "xlb_formulation": str(self.xlb_result.formulation),
            "xlb_velocity_set": str(self.xlb_result.velocity_set),
            "xlb_collision_model": str(self.xlb_result.collision_model),
            "xlb_streaming_scheme": str(self.xlb_result.streaming_scheme),
            "xlb_steps": int(self.xlb_result.n_steps),
            "xlb_converged": bool(self.xlb_result.converged),
            "xlb_convergence_metric": float(self.xlb_result.convergence_metric),
            "xlb_lattice_viscosity": float(self.xlb_result.lattice_viscosity),
            "xlb_p_inlet": float(self.xlb_result.lattice_pressure_inlet),
            "xlb_p_outlet": float(self.xlb_result.lattice_pressure_outlet),
            "xlb_rho_inlet": float(self.xlb_result.lattice_density_inlet),
            "xlb_rho_outlet": float(self.xlb_result.lattice_density_outlet),
            "xlb_dp_lattice": float(self.xlb_result.lattice_pressure_drop),
            "xlb_buffer_cells": int(self.xlb_result.inlet_outlet_buffer_cells),
            "xlb_u_superficial_lattice": float(self.xlb_result.superficial_velocity_lattice),
            "xlb_u_max_lattice": float(self.xlb_result.max_speed_lattice),
            "xlb_mach_max": float(self.xlb_result.max_mach_lattice),
            "xlb_re_voxel_max": float(self.xlb_result.reynolds_voxel_max),
        }

to_record

to_record()

Return scalar diagnostics suitable for tabulation.

Source code in src/voids/benchmarks/xlb.py
def to_record(self) -> dict[str, float | int | str | bool | None]:
    """Return scalar diagnostics suitable for tabulation."""

    k_voids = float((self.voids_result.permeability or {}).get(self.extract.flow_axis, np.nan))
    return {
        "flow_axis": self.extract.flow_axis,
        "phi_image": float(self.image_porosity),
        "phi_abs": float(self.absolute_porosity),
        "phi_eff": float(self.effective_porosity),
        "Np": int(self.extract.net.Np),
        "Nt": int(self.extract.net.Nt),
        "k_voids": k_voids,
        "k_xlb": float(self.xlb_result.permeability),
        "k_abs_diff": float(self.permeability_abs_diff),
        "k_rel_diff": float(self.permeability_rel_diff),
        "voids_mass_balance_error": float(self.voids_result.mass_balance_error),
        "conductance_model": str(self.options.conductance_model),
        "solver_voids": str(self.options.solver),
        "p_inlet_physical": float(self.bc.pin),
        "p_outlet_physical": float(self.bc.pout),
        "dp_physical": float(self.bc.pin - self.bc.pout),
        "extract_backend": str(self.extract.backend),
        "extract_backend_version": self.extract.backend_version,
        "xlb_backend": str(self.xlb_result.backend),
        "xlb_backend_version": self.xlb_result.backend_version,
        "xlb_formulation": str(self.xlb_result.formulation),
        "xlb_velocity_set": str(self.xlb_result.velocity_set),
        "xlb_collision_model": str(self.xlb_result.collision_model),
        "xlb_streaming_scheme": str(self.xlb_result.streaming_scheme),
        "xlb_steps": int(self.xlb_result.n_steps),
        "xlb_converged": bool(self.xlb_result.converged),
        "xlb_convergence_metric": float(self.xlb_result.convergence_metric),
        "xlb_lattice_viscosity": float(self.xlb_result.lattice_viscosity),
        "xlb_p_inlet": float(self.xlb_result.lattice_pressure_inlet),
        "xlb_p_outlet": float(self.xlb_result.lattice_pressure_outlet),
        "xlb_rho_inlet": float(self.xlb_result.lattice_density_inlet),
        "xlb_rho_outlet": float(self.xlb_result.lattice_density_outlet),
        "xlb_dp_lattice": float(self.xlb_result.lattice_pressure_drop),
        "xlb_buffer_cells": int(self.xlb_result.inlet_outlet_buffer_cells),
        "xlb_u_superficial_lattice": float(self.xlb_result.superficial_velocity_lattice),
        "xlb_u_max_lattice": float(self.xlb_result.max_speed_lattice),
        "xlb_mach_max": float(self.xlb_result.max_mach_lattice),
        "xlb_re_voxel_max": float(self.xlb_result.reynolds_voxel_max),
    }

solve_binary_volume_with_xlb

solve_binary_volume_with_xlb(
    phases, *, voxel_size, flow_axis=None, options=None
)

Solve a binary segmented volume directly with XLB and estimate permeability.

Parameters:

Name Type Description Default
phases ndarray

Binary segmented volume with void=1 and solid=0.

required
voxel_size float

Physical voxel size used when mapping lattice permeability back to physical units.

required
flow_axis str | None

Requested flow axis. When omitted, the canonical sample axis inferred by :func:voids.image.network_extraction.infer_sample_axes is used.

None
options XLBOptions | None

XLB numerical controls. This low-level interface expects lattice-unit pressure inputs through pressure_inlet_lattice, pressure_outlet_lattice, or pressure_drop_lattice.

None

Returns:

Type Description
XLBDirectSimulationResult

Direct-image XLB solution summary and permeability diagnostics.

Raises:

Type Description
ValueError

If the input volume, flow axis, or XLB numerical controls are invalid.

RuntimeError

If the run completes with a non-physical permeability estimate.

Notes

The current adapter uses XLB's incompressible Navier-Stokes lattice Boltzmann stepper with BGK-style collision and pressure / bounce-back boundary conditions. If options.formulation == "steady_stokes_limit", the same stepper is still used; the solution is simply driven more gently and interpreted in the low-Reynolds, low-Mach steady limit.

The inlet and outlet conditions are pressure boundary conditions. The public API accepts lattice pressure values or a lattice pressure drop, which are converted internally to the density values expected by the isothermal LBM boundary operator through p_lu = c_s^2 rho.

The permeability conversion is based on lattice units:

K_phys = nu_lu * U_lu * L_lu * dx_phys**2 / dp_lu

where U_lu is the superficial sample velocity, nu_lu is the lattice kinematic viscosity, L_lu is the voxel count along the flow axis, and dp_lu = p_in,lu - p_out,lu.

This keeps the benchmark focused on permeability, which is the transport quantity comparable to the PNM solve without requiring a full physical pressure-unit calibration of the lattice simulation.

Source code in src/voids/benchmarks/xlb.py
def solve_binary_volume_with_xlb(
    phases: np.ndarray,
    *,
    voxel_size: float,
    flow_axis: str | None = None,
    options: XLBOptions | None = None,
) -> XLBDirectSimulationResult:
    """Solve a binary segmented volume directly with XLB and estimate permeability.

    Parameters
    ----------
    phases :
        Binary segmented volume with ``void=1`` and ``solid=0``.
    voxel_size :
        Physical voxel size used when mapping lattice permeability back to
        physical units.
    flow_axis :
        Requested flow axis. When omitted, the canonical sample axis inferred by
        :func:`voids.image.network_extraction.infer_sample_axes` is used.
    options :
        XLB numerical controls. This low-level interface expects lattice-unit
        pressure inputs through ``pressure_inlet_lattice``,
        ``pressure_outlet_lattice``, or ``pressure_drop_lattice``.

    Returns
    -------
    XLBDirectSimulationResult
        Direct-image XLB solution summary and permeability diagnostics.

    Raises
    ------
    ValueError
        If the input volume, flow axis, or XLB numerical controls are invalid.
    RuntimeError
        If the run completes with a non-physical permeability estimate.

    Notes
    -----
    The current adapter uses XLB's incompressible Navier-Stokes lattice
    Boltzmann stepper with BGK-style collision and pressure / bounce-back
    boundary conditions. If ``options.formulation == "steady_stokes_limit"``,
    the same stepper is still used; the solution is simply driven more gently
    and interpreted in the low-Reynolds, low-Mach steady limit.

    The inlet and outlet conditions are pressure boundary conditions. The public
    API accepts lattice pressure values or a lattice pressure drop, which are
    converted internally to the density values expected by the isothermal LBM
    boundary operator through ``p_lu = c_s^2 rho``.

    The permeability conversion is based on lattice units:

    ``K_phys = nu_lu * U_lu * L_lu * dx_phys**2 / dp_lu``

    where ``U_lu`` is the superficial sample velocity, ``nu_lu`` is the lattice
    kinematic viscosity, ``L_lu`` is the voxel count along the flow axis, and
    ``dp_lu = p_in,lu - p_out,lu``.

    This keeps the benchmark focused on permeability, which is the transport
    quantity comparable to the PNM solve without requiring a full physical
    pressure-unit calibration of the lattice simulation.
    """

    arr = _as_binary_volume(phases)
    xlb_options = options or XLBOptions()

    if xlb_options.backend.lower() != "jax":
        raise ValueError("The current `voids` XLB adapter supports only backend='jax'")
    if xlb_options.formulation not in {"incompressible_navier_stokes", "steady_stokes_limit"}:
        raise ValueError(
            "formulation must be 'incompressible_navier_stokes' or 'steady_stokes_limit'"
        )
    if xlb_options.max_steps <= 0:
        raise ValueError("max_steps must be positive")
    if xlb_options.min_steps < 0:
        raise ValueError("min_steps must be non-negative")
    if xlb_options.check_interval <= 0:
        raise ValueError("check_interval must be positive")
    if xlb_options.steady_rtol <= 0:
        raise ValueError("steady_rtol must be positive")
    if xlb_options.lattice_viscosity <= 0:
        raise ValueError("lattice_viscosity must be positive")
    if xlb_options.inlet_outlet_buffer_cells < 0:
        raise ValueError("inlet_outlet_buffer_cells must be non-negative")
    pressure_inlet_lattice, pressure_outlet_lattice = _resolve_lattice_pressure_bc(
        xlb_options,
        cs2=ISOTHERMAL_LATTICE_CS2,
    )
    density_inlet_lattice = pressure_inlet_lattice / ISOTHERMAL_LATTICE_CS2
    density_outlet_lattice = pressure_outlet_lattice / ISOTHERMAL_LATTICE_CS2
    density_drop_lattice = density_inlet_lattice - density_outlet_lattice
    if xlb_options.formulation == "steady_stokes_limit" and density_drop_lattice > 5.0e-4:
        warnings.warn(
            "steady_stokes_limit is being used with a relatively large lattice "
            "pressure drop. The run still uses the same incompressible LBM stepper, "
            "so keep the imposed driving small if the goal is a steady Stokes-limit "
            "reference.",
            RuntimeWarning,
            stacklevel=2,
        )

    _, axis_lengths, axis_areas, inferred_axis = infer_sample_axes(arr.shape, voxel_size=voxel_size)
    axis = inferred_axis if flow_axis is None else flow_axis
    axis_index = _axis_to_index(axis, arr.ndim)

    aligned_void_sample = np.moveaxis(np.asarray(arr, dtype=bool), axis_index, 0)
    if not np.any(aligned_void_sample[0]):
        raise ValueError("The inlet plane contains no void voxels for the requested flow axis")
    if not np.any(aligned_void_sample[-1]):
        raise ValueError("The outlet plane contains no void voxels for the requested flow axis")

    buffer_cells = int(xlb_options.inlet_outlet_buffer_cells)
    if buffer_cells > 0:
        aligned_void = np.pad(
            aligned_void_sample,
            pad_width=((buffer_cells, buffer_cells),) + ((0, 0),) * (aligned_void_sample.ndim - 1),
            mode="constant",
            constant_values=True,
        )
    else:
        aligned_void = aligned_void_sample

    xlb_api = _import_xlb()
    jax = xlb_api["jax"]
    xlb = xlb_api["xlb"]
    ComputeBackend = xlb_api["ComputeBackend"]
    PrecisionPolicy = xlb_api["PrecisionPolicy"]
    IncompressibleNavierStokesStepper = xlb_api["IncompressibleNavierStokesStepper"]
    HalfwayBounceBackBC = xlb_api["HalfwayBounceBackBC"]
    RegularizedBC = xlb_api["RegularizedBC"]
    velocity_set_cls = xlb_api["D2Q9"] if arr.ndim == 2 else xlb_api["D3Q19"]

    precision_policy = getattr(PrecisionPolicy, xlb_options.precision_policy, None)
    if precision_policy is None:
        raise ValueError(f"Unknown XLB precision policy {xlb_options.precision_policy!r}")

    compute_backend = ComputeBackend.JAX
    velocity_set = velocity_set_cls(precision_policy, compute_backend)
    xlb.init(velocity_set, compute_backend, precision_policy)
    grid = xlb.grid.grid_factory(aligned_void.shape, compute_backend=compute_backend)

    sealed_side_mask = np.zeros_like(aligned_void, dtype=bool)
    for side_axis in range(1, aligned_void.ndim):
        lower: list[slice | int] = [slice(None)] * aligned_void.ndim
        upper: list[slice | int] = [slice(None)] * aligned_void.ndim
        lower[side_axis] = 0
        upper[side_axis] = -1
        sealed_side_mask[tuple(lower)] = True
        sealed_side_mask[tuple(upper)] = True

    inlet_mask = np.zeros_like(aligned_void, dtype=bool)
    outlet_mask = np.zeros_like(aligned_void, dtype=bool)
    # Pressure BCs are imposed on planar reservoir faces restricted to void voxels,
    # with side-wall edges excluded because those cells belong to the sealed sample
    # jacket.  Intersecting with ``aligned_void`` prevents solid voxels from being
    # assigned a pressure BC, which would otherwise "open" them and corrupt the
    # bounce-back mask assignment below.
    inlet_mask[0, ...] = aligned_void[0, ...] & ~sealed_side_mask[0, ...]
    outlet_mask[-1, ...] = aligned_void[-1, ...] & ~sealed_side_mask[-1, ...]
    if not np.any(inlet_mask):
        raise ValueError(
            "The trimmed inlet plane has no interior void voxels for the requested flow axis"
        )
    if not np.any(outlet_mask):
        raise ValueError(
            "The trimmed outlet plane has no interior void voxels for the requested flow axis"
        )

    bounceback_mask = ~aligned_void
    bounceback_mask |= sealed_side_mask
    bounceback_mask &= ~(inlet_mask | outlet_mask)

    inlet_indices = _mask_to_indices(inlet_mask)
    outlet_indices = _mask_to_indices(outlet_mask)
    bounceback_indices = _mask_to_indices(bounceback_mask)

    boundary_conditions = [
        RegularizedBC(
            "pressure",
            prescribed_value=float(pressure_inlet_lattice / float(np.asarray(velocity_set.cs2))),
            indices=inlet_indices,
        ),
        RegularizedBC(
            "pressure",
            prescribed_value=float(pressure_outlet_lattice / float(np.asarray(velocity_set.cs2))),
            indices=outlet_indices,
        ),
    ]
    if bounceback_indices is not None:
        boundary_conditions.append(HalfwayBounceBackBC(indices=bounceback_indices))

    stepper = IncompressibleNavierStokesStepper(
        grid=grid,
        boundary_conditions=boundary_conditions,
        collision_type=xlb_options.collision_model,
        streaming_scheme=xlb_options.streaming_scheme,
    )
    f_0, f_1, bc_mask, missing_mask = stepper.prepare_fields()

    omega_float = 1.0 / (3.0 * float(xlb_options.lattice_viscosity) + 0.5)
    omega = np.asarray(omega_float, dtype=precision_policy.compute_precision.jax_dtype)

    velocity_aligned = np.zeros((arr.ndim, *aligned_void_sample.shape), dtype=float)
    axial_velocity_aligned = np.zeros_like(aligned_void, dtype=float)
    superficial_profile = np.zeros(aligned_void.shape[0], dtype=float)
    superficial_velocity = 0.0
    max_speed_lattice = 0.0
    convergence_metric = np.inf
    previous_superficial_velocity: float | None = None
    converged = False
    n_steps = 0

    # f_current is a JAX array; typed as `object` because `jaxlib` types are
    # not available in the default mypy environment (jax is an optional dep).
    def _measure_current_state(
        f_current: object,
    ) -> tuple[np.ndarray, np.ndarray, np.ndarray, float, float]:
        jax.block_until_ready(f_current)
        _, u = stepper.macroscopic(f_current)
        u_full = np.asarray(u, dtype=float)
        axial_full = np.asarray(u_full[0], dtype=float)
        speed_full = np.linalg.norm(u_full, axis=0)
        sample_slice = slice(buffer_cells, buffer_cells + aligned_void_sample.shape[0])
        velocity = np.asarray(u_full[:, sample_slice, ...], dtype=float)
        axial = axial_full[sample_slice, ...]
        speed = speed_full[sample_slice, ...]
        profile = _superficial_velocity_profile(axial, aligned_void_sample)
        interior_profile = profile[1:-1] if profile.size > 2 else profile
        mean_superficial_velocity = float(np.mean(interior_profile))
        max_speed = float(np.max(speed[aligned_void_sample]))
        return velocity, axial, profile, mean_superficial_velocity, max_speed

    for step in range(xlb_options.max_steps):
        f_0, f_1 = stepper(f_0, f_1, bc_mask, missing_mask, omega, step)
        f_0, f_1 = f_1, f_0
        n_steps = step + 1

        if n_steps % xlb_options.check_interval != 0 and n_steps != xlb_options.max_steps:
            continue

        (
            velocity_aligned,
            axial_velocity_aligned,
            superficial_profile,
            superficial_velocity,
            max_speed_lattice,
        ) = _measure_current_state(f_0)
        if previous_superficial_velocity is not None:
            convergence_metric = abs(superficial_velocity - previous_superficial_velocity) / max(
                abs(previous_superficial_velocity),
                1.0e-30,
            )
        previous_superficial_velocity = superficial_velocity

        if (
            n_steps >= xlb_options.min_steps
            and np.isfinite(convergence_metric)
            and convergence_metric < xlb_options.steady_rtol
        ):
            converged = True
            break

    if not np.any(axial_velocity_aligned):
        (
            velocity_aligned,
            axial_velocity_aligned,
            superficial_profile,
            superficial_velocity,
            max_speed_lattice,
        ) = _measure_current_state(f_0)
        if previous_superficial_velocity is not None:
            convergence_metric = abs(superficial_velocity - previous_superficial_velocity) / max(
                abs(previous_superficial_velocity),
                1.0e-30,
            )

    lattice_pressure_drop = float(pressure_inlet_lattice) - float(pressure_outlet_lattice)
    permeability = (
        float(xlb_options.lattice_viscosity)
        * float(superficial_velocity)
        * float(axis_lengths[axis])
        * float(voxel_size)
        / float(lattice_pressure_drop)
    )
    cs_lattice = float(np.sqrt(np.asarray(velocity_set.cs2)))
    max_mach_lattice = float(max_speed_lattice) / cs_lattice
    reynolds_voxel_max = float(max_speed_lattice) / float(xlb_options.lattice_viscosity)
    if not converged:
        warnings.warn(
            "XLB direct-image benchmark did not satisfy the steady-state tolerance "
            f"after {n_steps} steps; the reported permeability may be biased. "
            f"Last relative velocity change: {convergence_metric:.3e}.",
            RuntimeWarning,
            stacklevel=2,
        )
    if not np.isfinite(permeability) or permeability <= 0.0:
        raise RuntimeError(
            "XLB produced a non-physical permeability estimate "
            f"({permeability:.6e} m^2-equivalent). "
            "This usually indicates incompatible boundary conditions, an "
            "insufficient inlet/outlet buffer, or a run that is too short to "
            "reach steady state."
        )

    velocity_spatial_original = np.moveaxis(velocity_aligned, 1, axis_index + 1)
    aligned_axes = [axis_index, *[idx for idx in range(arr.ndim) if idx != axis_index]]
    velocity_original = np.empty_like(velocity_spatial_original)
    for aligned_component, original_component in enumerate(aligned_axes):
        velocity_original[original_component] = velocity_spatial_original[aligned_component]

    axial_velocity_original = np.asarray(velocity_original[axis_index], dtype=float)

    return XLBDirectSimulationResult(
        flow_axis=axis,
        voxel_size=float(voxel_size),
        image_porosity=float(arr.mean()),
        sample_lengths=axis_lengths,
        sample_cross_sections=axis_areas,
        lattice_viscosity=float(xlb_options.lattice_viscosity),
        lattice_pressure_inlet=float(pressure_inlet_lattice),
        lattice_pressure_outlet=float(pressure_outlet_lattice),
        lattice_density_inlet=float(pressure_inlet_lattice / float(np.asarray(velocity_set.cs2))),
        lattice_density_outlet=float(pressure_outlet_lattice / float(np.asarray(velocity_set.cs2))),
        lattice_pressure_drop=float(lattice_pressure_drop),
        inlet_outlet_buffer_cells=buffer_cells,
        omega=omega_float,
        superficial_velocity_lattice=float(superficial_velocity),
        superficial_velocity_profile_lattice=np.asarray(superficial_profile, dtype=float),
        velocity_lattice=np.asarray(velocity_original, dtype=float),
        axial_velocity_lattice=np.asarray(axial_velocity_original, dtype=float),
        converged=bool(converged),
        n_steps=int(n_steps),
        convergence_metric=float(convergence_metric),
        permeability=float(permeability),
        backend="jax",
        backend_version=getattr(xlb, "__version__", None),
        formulation=str(xlb_options.formulation),
        velocity_set=str(velocity_set_cls.__name__),
        collision_model=str(xlb_options.collision_model),
        streaming_scheme=str(xlb_options.streaming_scheme),
        max_speed_lattice=float(max_speed_lattice),
        max_mach_lattice=float(max_mach_lattice),
        reynolds_voxel_max=float(reynolds_voxel_max),
    )

benchmark_segmented_volume_with_xlb

benchmark_segmented_volume_with_xlb(
    phases,
    *,
    voxel_size,
    flow_axis=None,
    fluid=None,
    delta_p=None,
    pin=None,
    pout=None,
    options=None,
    xlb_options=None,
    length_unit="m",
    pressure_unit="Pa",
    extraction_kwargs=None,
    provenance_notes=None,
    strict=True,
)

Benchmark a segmented volume against a direct-image XLB solve.

Parameters:

Name Type Description Default
phases ndarray

Binary segmented volume with void=1 and solid=0.

required
voxel_size float

Physical voxel size of the segmented image.

required
flow_axis str | None

Requested macroscopic flow axis. When omitted, the extracted sample axis is inferred from the image shape.

None
fluid FluidSinglePhase | None

Single-phase fluid properties. A positive density is required here because the shared physical pressure drop must be mapped into lattice pressure units for XLB.

None
delta_p float | None

Preferred physical pressure drop for the benchmark, typically in Pa. When provided alone, the wrapper uses pout = 0 and pin = delta_p as a gauge choice. When combined with one of pin or pout, the missing value is inferred. When combined with both, consistency with pin - pout is enforced.

None
pin float | None

Optional absolute physical inlet and outlet pressures. They are kept for backward compatibility and for cases where a particular physical reference level should be recorded. For the current incompressible permeability benchmark, only the pressure drop pin - pout enters the lattice-pressure coupling.

None
pout float | None

Optional absolute physical inlet and outlet pressures. They are kept for backward compatibility and for cases where a particular physical reference level should be recorded. For the current incompressible permeability benchmark, only the pressure drop pin - pout enters the lattice-pressure coupling.

None
options SinglePhaseOptions | None

voids single-phase solver options used on the extracted network.

None
xlb_options XLBOptions | None

XLB options template. For this high-level wrapper the lattice pressure fields are overwritten so the direct-image XLB run matches the resolved physical pressure drop.

None
length_unit str

Metadata passed through the network-extraction provenance.

'm'
pressure_unit str

Metadata passed through the network-extraction provenance.

'm'
extraction_kwargs dict[str, object] | None

Optional keyword arguments forwarded to :func:voids.image.network_extraction.extract_spanning_pore_network.

None
provenance_notes dict[str, object] | None

Optional provenance metadata stored on the extracted-network result.

None
strict bool

Extraction strictness flag forwarded to the extraction layer.

True

Returns:

Type Description
SegmentedVolumeXLBResult

Joint record containing extraction results, the voids solve, the pressure-coupled XLB solve, and permeability mismatch metrics.

Raises:

Type Description
ValueError

If the image is invalid, the extracted spanning network lacks usable inlet/outlet pores, the pressure specification is inconsistent, the implied pressure drop is not positive, or the fluid density is missing for the physical-to-lattice pressure mapping.

Notes

The voids side solves on the extracted pore network. The XLB side solves directly on the binary segmented image. This is the scientifically relevant comparison if the goal is to assess extraction loss and PNM model discrepancy against a higher-fidelity voxel-scale reference.

The crucial consistency condition enforced here is that the same physical pressure drop is used for both the voids and XLB sides of the benchmark. The absolute pressure offset is numerically immaterial for the current incompressible permeability benchmark. For example, delta_p=1 with the default gauge and delta_p=1 with pin=101326 / pout=101325 drive the same benchmark permeability comparison.

Source code in src/voids/benchmarks/xlb.py
def benchmark_segmented_volume_with_xlb(
    phases: np.ndarray,
    *,
    voxel_size: float,
    flow_axis: str | None = None,
    fluid: FluidSinglePhase | None = None,
    delta_p: float | None = None,
    pin: float | None = None,
    pout: float | None = None,
    options: SinglePhaseOptions | None = None,
    xlb_options: XLBOptions | None = None,
    length_unit: str = "m",
    pressure_unit: str = "Pa",
    extraction_kwargs: dict[str, object] | None = None,
    provenance_notes: dict[str, object] | None = None,
    strict: bool = True,
) -> SegmentedVolumeXLBResult:
    """Benchmark a segmented volume against a direct-image XLB solve.

    Parameters
    ----------
    phases :
        Binary segmented volume with ``void=1`` and ``solid=0``.
    voxel_size :
        Physical voxel size of the segmented image.
    flow_axis :
        Requested macroscopic flow axis. When omitted, the extracted sample axis
        is inferred from the image shape.
    fluid :
        Single-phase fluid properties. A positive density is required here
        because the shared physical pressure drop must be mapped into lattice
        pressure units for XLB.
    delta_p :
        Preferred physical pressure drop for the benchmark, typically in Pa.
        When provided alone, the wrapper uses `pout = 0` and `pin = delta_p` as
        a gauge choice. When combined with one of `pin` or `pout`, the missing
        value is inferred. When combined with both, consistency with
        ``pin - pout`` is enforced.
    pin, pout :
        Optional absolute physical inlet and outlet pressures. They are kept for
        backward compatibility and for cases where a particular physical
        reference level should be recorded. For the current incompressible
        permeability benchmark, only the pressure drop ``pin - pout`` enters the
        lattice-pressure coupling.
    options :
        `voids` single-phase solver options used on the extracted network.
    xlb_options :
        XLB options template. For this high-level wrapper the lattice pressure
        fields are overwritten so the direct-image XLB run matches the resolved
        physical pressure drop.
    length_unit, pressure_unit :
        Metadata passed through the network-extraction provenance.
    extraction_kwargs :
        Optional keyword arguments forwarded to
        :func:`voids.image.network_extraction.extract_spanning_pore_network`.
    provenance_notes :
        Optional provenance metadata stored on the extracted-network result.
    strict :
        Extraction strictness flag forwarded to the extraction layer.

    Returns
    -------
    SegmentedVolumeXLBResult
        Joint record containing extraction results, the `voids` solve, the
        pressure-coupled XLB solve, and permeability mismatch metrics.

    Raises
    ------
    ValueError
        If the image is invalid, the extracted spanning network lacks usable
        inlet/outlet pores, the pressure specification is inconsistent, the
        implied pressure drop is not positive, or the fluid density is missing
        for the physical-to-lattice pressure mapping.

    Notes
    -----
    The `voids` side solves on the extracted pore network. The XLB side solves
    directly on the binary segmented image. This is the scientifically relevant
    comparison if the goal is to assess extraction loss and PNM model discrepancy
    against a higher-fidelity voxel-scale reference.

    The crucial consistency condition enforced here is that the same physical
    pressure drop is used for both the `voids` and XLB sides of the benchmark.
    The absolute pressure offset is numerically immaterial for the current
    incompressible permeability benchmark. For example, ``delta_p=1`` with the
    default gauge and ``delta_p=1`` with ``pin=101326`` / ``pout=101325`` drive
    the same benchmark permeability comparison.
    """

    arr = _as_binary_volume(phases)
    image_phi = float(arr.mean())
    pin_used, pout_used, delta_p_physical = resolve_benchmark_pressures(
        delta_p=delta_p,
        pin=pin,
        pout=pout,
    )

    notes = dict(provenance_notes or {})
    notes.setdefault("benchmark_kind", "segmented_volume_xlb")

    extract = extract_spanning_pore_network(
        arr,
        voxel_size=voxel_size,
        flow_axis=flow_axis,
        length_unit=length_unit,
        pressure_unit=pressure_unit,
        extraction_kwargs=extraction_kwargs,
        provenance_notes=notes,
        strict=strict,
    )

    fluid_used = fluid or FluidSinglePhase(viscosity=1.0e-3, density=1.0e3)
    options_used = options or SinglePhaseOptions(
        conductance_model="valvatne_blunt",
        solver="direct",
    )
    xlb_options_used = xlb_options or XLBOptions()

    axis = extract.flow_axis
    inlet_count = int(
        np.asarray(extract.net.pore_labels.get(f"inlet_{axis}min", []), dtype=bool).sum()
    )
    outlet_count = int(
        np.asarray(extract.net.pore_labels.get(f"outlet_{axis}max", []), dtype=bool).sum()
    )
    if extract.net.Np == 0 or inlet_count == 0 or outlet_count == 0:
        raise ValueError(
            "The extracted spanning network is empty or lacks non-empty inlet/outlet pore labels "
            f"for axis '{axis}', so the XLB benchmark cannot be compared against `voids` on this case."
        )

    if fluid_used.density is None or fluid_used.density <= 0.0:
        raise ValueError(
            "benchmark_segmented_volume_with_xlb requires `fluid.density` to map the shared "
            "physical pressure drop into lattice pressure units"
        )
    xlb_options_coupled = _couple_xlb_options_to_physical_pressure_drop(
        xlb_options_used,
        delta_p_physical=delta_p_physical,
        voxel_size=voxel_size,
        fluid=fluid_used,
    )

    bc = make_benchmark_pressure_bc(axis, pin=pin_used, pout=pout_used)
    voids_result = solve(
        extract.net,
        fluid=fluid_used,
        bc=bc,
        axis=axis,
        options=options_used,
    )
    xlb_result = solve_binary_volume_with_xlb(
        arr,
        voxel_size=voxel_size,
        flow_axis=axis,
        options=xlb_options_coupled,
    )

    k_voids = float((voids_result.permeability or {}).get(axis, np.nan))
    k_xlb = float(xlb_result.permeability)

    return SegmentedVolumeXLBResult(
        extract=extract,
        fluid=fluid_used,
        bc=bc,
        options=options_used,
        xlb_options=xlb_options_coupled,
        image_porosity=image_phi,
        absolute_porosity=float(absolute_porosity(extract.net)),
        effective_porosity=float(effective_porosity(extract.net, axis=axis)),
        voids_result=voids_result,
        xlb_result=xlb_result,
        permeability_abs_diff=abs(k_voids - k_xlb),
        permeability_rel_diff=_rel_diff(k_voids, k_xlb),
    )