Skip to content

I/O

The voids.io sub-package handles serialization and import/export of networks.


HDF5

voids.io.hdf5

save_hdf5

save_hdf5(net, path)

Serialize a network to the project HDF5 interchange format.

Parameters:

Name Type Description Default
net Network

Network to store.

required
path str | Path

Destination file path. Parent directories must already exist.

required
Notes

The file layout is intentionally explicit:

  • /meta stores schema and provenance metadata.
  • /sample stores the sample geometry payload.
  • /network/pore and /network/throat store arrays.
  • /labels stores boolean pore and throat labels as uint8 datasets.
  • / attribute extra stores JSON-compatible auxiliary metadata.
Source code in src/voids/io/hdf5.py
def save_hdf5(net: Network, path: str | Path) -> None:
    """Serialize a network to the project HDF5 interchange format.

    Parameters
    ----------
    net :
        Network to store.
    path :
        Destination file path. Parent directories must already exist.

    Notes
    -----
    The file layout is intentionally explicit:

    - ``/meta`` stores schema and provenance metadata.
    - ``/sample`` stores the sample geometry payload.
    - ``/network/pore`` and ``/network/throat`` store arrays.
    - ``/labels`` stores boolean pore and throat labels as ``uint8`` datasets.
    - ``/`` attribute ``extra`` stores JSON-compatible auxiliary metadata.
    """

    path = Path(path)
    with h5py.File(path, "w") as f:
        meta = f.create_group("meta")
        meta.create_dataset("schema_version", data=np.bytes_(net.schema_version))
        _write_json_attr(meta, "provenance", net.provenance.to_metadata())

        sample = f.create_group("sample")
        _write_json_attr(sample, "payload", net.sample.to_metadata())

        ng = f.create_group("network")
        pg = ng.create_group("pore")
        tg = ng.create_group("throat")
        pg.create_dataset("coords", data=net.pore_coords)
        tg.create_dataset("conns", data=net.throat_conns)
        for k, v in net.pore.items():
            pg.create_dataset(k, data=v)
        for k, v in net.throat.items():
            tg.create_dataset(k, data=v)

        labels = f.create_group("labels")
        lpg = labels.create_group("pore")
        ltg = labels.create_group("throat")
        for k, v in net.pore_labels.items():
            lpg.create_dataset(k, data=v.astype(np.uint8))
        for k, v in net.throat_labels.items():
            ltg.create_dataset(k, data=v.astype(np.uint8))

        _write_json_attr(f, "extra", net.extra)

load_hdf5

load_hdf5(path)

Load a network from the project HDF5 interchange format.

Parameters:

Name Type Description Default
path str | Path

Path to an HDF5 file produced by :func:save_hdf5.

required

Returns:

Type Description
Network

Reconstructed network object.

Notes

Boolean labels are stored on disk as uint8 arrays for portability and are converted back to bool arrays during import.

Source code in src/voids/io/hdf5.py
def load_hdf5(path: str | Path) -> Network:
    """Load a network from the project HDF5 interchange format.

    Parameters
    ----------
    path :
        Path to an HDF5 file produced by :func:`save_hdf5`.

    Returns
    -------
    Network
        Reconstructed network object.

    Notes
    -----
    Boolean labels are stored on disk as ``uint8`` arrays for portability and are
    converted back to ``bool`` arrays during import.
    """

    path = Path(path)
    with h5py.File(path, "r") as f:
        schema_version = f["meta"]["schema_version"][()].decode("utf-8")
        prov = Provenance.from_metadata(_read_json_attr(f["meta"], "provenance", {}))
        sample = SampleGeometry.from_metadata(_read_json_attr(f["sample"], "payload", {}))

        pore_coords = f["network"]["pore"]["coords"][()]
        throat_conns = f["network"]["throat"]["conns"][()]
        pore = {k: ds[()] for k, ds in f["network"]["pore"].items() if k != "coords"}
        throat = {k: ds[()] for k, ds in f["network"]["throat"].items() if k != "conns"}
        pore_labels = (
            {k: ds[()].astype(bool) for k, ds in f["labels"]["pore"].items()}
            if "labels" in f
            else {}
        )
        throat_labels = (
            {k: ds[()].astype(bool) for k, ds in f["labels"]["throat"].items()}
            if "labels" in f
            else {}
        )
        extra = _read_json_attr(f, "extra", {})

    return Network(
        throat_conns=throat_conns,
        pore_coords=pore_coords,
        sample=sample,
        provenance=prov,
        schema_version=schema_version,
        pore=pore,
        throat=throat,
        pore_labels=pore_labels,
        throat_labels=throat_labels,
        extra=extra,
    )

PoreSpy Import

voids.io.porespy

scale_porespy_geometry

scale_porespy_geometry(network_dict, *, voxel_size)

Scale common PoreSpy geometry fields from voxel units to physical units.

Parameters:

Name Type Description Default
network_dict Mapping[str, object]

PoreSpy/OpenPNM-style mapping containing keys such as pore.coords, throat.cross_sectional_area and pore.volume.

required
voxel_size float

Edge length of one voxel in physical units.

required

Returns:

Type Description
dict of str to object

New mapping with common geometric fields rescaled.

Raises:

Type Description
ValueError

If voxel_size is not positive.

Notes

This helper assumes isotropic voxels. The conversion factors are:

  • lengths: L_phys = L_vox * voxel_size
  • areas: A_phys = A_vox * voxel_size**2
  • volumes: V_phys = V_vox * voxel_size**3
  • perimeters: P_phys = P_vox * voxel_size

When throat.volume is absent but throat.cross_sectional_area and throat.total_length are available, a simple conduit approximation is used:

throat.volume = throat.cross_sectional_area * throat.total_length

This is convenient for manufactured examples and notebook workflows, but it is still a geometric approximation rather than an exact segmented volume.

Source code in src/voids/io/porespy.py
def scale_porespy_geometry(
    network_dict: Mapping[str, object], *, voxel_size: float
) -> dict[str, object]:
    """Scale common PoreSpy geometry fields from voxel units to physical units.

    Parameters
    ----------
    network_dict :
        PoreSpy/OpenPNM-style mapping containing keys such as ``pore.coords``,
        ``throat.cross_sectional_area`` and ``pore.volume``.
    voxel_size :
        Edge length of one voxel in physical units.

    Returns
    -------
    dict of str to object
        New mapping with common geometric fields rescaled.

    Raises
    ------
    ValueError
        If ``voxel_size`` is not positive.

    Notes
    -----
    This helper assumes isotropic voxels. The conversion factors are:

    - lengths: ``L_phys = L_vox * voxel_size``
    - areas: ``A_phys = A_vox * voxel_size**2``
    - volumes: ``V_phys = V_vox * voxel_size**3``
    - perimeters: ``P_phys = P_vox * voxel_size``

    When ``throat.volume`` is absent but ``throat.cross_sectional_area`` and
    ``throat.total_length`` are available, a simple conduit approximation is used:

    ``throat.volume = throat.cross_sectional_area * throat.total_length``

    This is convenient for manufactured examples and notebook workflows, but it is
    still a geometric approximation rather than an exact segmented volume.
    """

    L = float(voxel_size)
    if L <= 0:
        raise ValueError("voxel_size must be positive")

    scaled = dict(network_dict)
    for key, value in list(scaled.items()):
        arr = np.asarray(value)
        if not np.issubdtype(arr.dtype, np.number):
            continue
        if key in _AREA_KEYS:
            scaled[key] = arr.astype(float) * L**2
        elif key in _LENGTH_KEYS:
            scaled[key] = arr.astype(float) * L
        elif key in _VOLUME_KEYS:
            scaled[key] = arr.astype(float) * L**3
        elif key in _PERIMETER_KEYS:
            scaled[key] = arr.astype(float) * L

    if "throat.volume" not in scaled and all(
        key in scaled for key in ("throat.cross_sectional_area", "throat.total_length")
    ):
        scaled["throat.volume"] = np.asarray(
            scaled["throat.cross_sectional_area"], dtype=float
        ) * np.asarray(scaled["throat.total_length"], dtype=float)
    if "pore.volume" not in scaled and "pore.region_volume" in scaled:
        scaled["pore.volume"] = np.asarray(scaled["pore.region_volume"], dtype=float)
    return scaled

ensure_cartesian_boundary_labels

ensure_cartesian_boundary_labels(
    network_dict, *, axes=None, tol_fraction=0.05
)

Infer Cartesian inlet and outlet pore labels from coordinates.

Parameters:

Name Type Description Default
network_dict Mapping[str, object]

Mapping containing at least pore.coords.

required
axes tuple[str, ...] | None

Axes to label. If omitted, all axes present in the coordinate array are used.

None
tol_fraction float

Fraction of the domain span used as a geometric tolerance near each boundary.

0.05

Returns:

Type Description
dict of str to object

Updated mapping with labels such as pore.inlet_xmin and pore.outlet_xmax.

Raises:

Type Description
ValueError

If the coordinate array has invalid shape, if tol_fraction is negative, or if an invalid axis name is requested.

Notes

For each active axis, the helper marks pores satisfying

  • x <= x_min + tol as inlet pores
  • x >= x_max - tol as outlet pores

where tol = tol_fraction * max(x_max - x_min, 1e-12).

Source code in src/voids/io/porespy.py
def ensure_cartesian_boundary_labels(
    network_dict: Mapping[str, object],
    *,
    axes: tuple[str, ...] | None = None,
    tol_fraction: float = 0.05,
) -> dict[str, object]:
    """Infer Cartesian inlet and outlet pore labels from coordinates.

    Parameters
    ----------
    network_dict :
        Mapping containing at least ``pore.coords``.
    axes :
        Axes to label. If omitted, all axes present in the coordinate array are used.
    tol_fraction :
        Fraction of the domain span used as a geometric tolerance near each boundary.

    Returns
    -------
    dict of str to object
        Updated mapping with labels such as ``pore.inlet_xmin`` and ``pore.outlet_xmax``.

    Raises
    ------
    ValueError
        If the coordinate array has invalid shape, if ``tol_fraction`` is negative,
        or if an invalid axis name is requested.

    Notes
    -----
    For each active axis, the helper marks pores satisfying

    - ``x <= x_min + tol`` as inlet pores
    - ``x >= x_max - tol`` as outlet pores

    where ``tol = tol_fraction * max(x_max - x_min, 1e-12)``.
    """

    coords = np.asarray(network_dict["pore.coords"], dtype=float)
    if coords.ndim != 2 or coords.shape[1] not in {2, 3}:
        raise ValueError("pore.coords must have shape (Np, 2) or (Np, 3)")
    if tol_fraction < 0:
        raise ValueError("tol_fraction must be nonnegative")

    ndim = coords.shape[1]
    active_axes = axes if axes is not None else tuple(("x", "y", "z")[:ndim])
    updated = dict(network_dict)
    boundary = np.asarray(
        updated.get("pore.boundary", np.zeros(coords.shape[0], dtype=bool)), dtype=bool
    ).copy()

    for axis in active_axes:
        if axis not in _AXIS_INDEX:
            raise ValueError("axes entries must be drawn from {'x', 'y', 'z'}")
        axis_index = _AXIS_INDEX[axis]
        if axis_index >= ndim:
            raise ValueError(
                f"axis '{axis}' is not available in pore.coords with shape {coords.shape}"
            )
        values = coords[:, axis_index]
        # Use argmin/argmax instead of ndarray.min/max for compatibility with
        # environments where numpy reduction defaults are monkeypatched.
        lo = float(values[np.argmin(values)])
        hi = float(values[np.argmax(values)])
        tol = tol_fraction * max(hi - lo, 1e-12)
        inlet_key = f"pore.inlet_{axis}min"
        outlet_key = f"pore.outlet_{axis}max"
        updated.setdefault(inlet_key, values <= lo + tol)
        updated.setdefault(outlet_key, values >= hi - tol)
        boundary |= np.asarray(updated[inlet_key], dtype=bool) | np.asarray(
            updated[outlet_key], dtype=bool
        )

    updated["pore.boundary"] = boundary
    return updated

from_porespy

from_porespy(
    network_dict,
    *,
    sample=None,
    provenance=None,
    strict=True,
    geometry_repairs=None,
    repair_seed=0,
)

Build a :class:Network from a PoreSpy/OpenPNM-style mapping.

Parameters:

Name Type Description Default
network_dict Mapping[str, object]

Mapping containing PoreSpy/OpenPNM keys such as pore.coords and throat.conns.

required
sample SampleGeometry | None

Sample geometry metadata attached to the resulting network. If omitted, a default empty :class:SampleGeometry is used.

None
provenance Provenance | None

Provenance metadata. If omitted, a default record with source_kind="porespy" is created.

None
strict bool

If True, missing topology keys immediately raise an error.

True
geometry_repairs str | None

Optional extraction-style preprocessing mode. Set to "imperial_export" to apply the Imperial College export heuristics for throat shape-factor repair and pore shape-factor reconstruction. The default None preserves the imported geometry as-is apart from basic alias normalization. The legacy name "pnextract" is still accepted as a deprecated alias for backward compatibility.

None
repair_seed int | None

Seed for any stochastic repair branch. Only used when geometry_repairs="imperial_export".

0

Returns:

Type Description
Network

Imported network in the canonical voids representation.

Raises:

Type Description
KeyError

If the required topology keys are missing.

Notes

The importer performs several normalizations:

  • PoreSpy/OpenPNM aliases are mapped to canonical voids names.
  • Two-dimensional coordinates are embedded into 3D as (x, y, 0).
  • Basic missing geometry is derived when possible.
  • Common boundary aliases such as left and right are mirrored to inlet_xmin and outlet_xmax.

Unsupported nested arrays such as throat.hydraulic_size_factors are preserved in net.extra so that information is not silently lost.

Source code in src/voids/io/porespy.py
def from_porespy(
    network_dict: Mapping[str, object],
    *,
    sample: SampleGeometry | None = None,
    provenance: Provenance | None = None,
    strict: bool = True,
    geometry_repairs: str | None = None,
    repair_seed: int | None = 0,
) -> Network:
    """Build a :class:`Network` from a PoreSpy/OpenPNM-style mapping.

    Parameters
    ----------
    network_dict :
        Mapping containing PoreSpy/OpenPNM keys such as ``pore.coords`` and
        ``throat.conns``.
    sample :
        Sample geometry metadata attached to the resulting network. If omitted,
        a default empty :class:`SampleGeometry` is used.
    provenance :
        Provenance metadata. If omitted, a default record with
        ``source_kind="porespy"`` is created.
    strict :
        If ``True``, missing topology keys immediately raise an error.
    geometry_repairs :
        Optional extraction-style preprocessing mode. Set to
        ``"imperial_export"`` to apply the Imperial College export heuristics
        for throat shape-factor repair and pore shape-factor reconstruction.
        The default ``None`` preserves the imported geometry as-is apart from
        basic alias normalization. The legacy name ``"pnextract"`` is still
        accepted as a deprecated alias for backward compatibility.
    repair_seed :
        Seed for any stochastic repair branch. Only used when
        ``geometry_repairs="imperial_export"``.

    Returns
    -------
    Network
        Imported network in the canonical ``voids`` representation.

    Raises
    ------
    KeyError
        If the required topology keys are missing.

    Notes
    -----
    The importer performs several normalizations:

    - PoreSpy/OpenPNM aliases are mapped to canonical ``voids`` names.
    - Two-dimensional coordinates are embedded into 3D as ``(x, y, 0)``.
    - Basic missing geometry is derived when possible.
    - Common boundary aliases such as ``left`` and ``right`` are mirrored to
      ``inlet_xmin`` and ``outlet_xmax``.

    Unsupported nested arrays such as ``throat.hydraulic_size_factors`` are
    preserved in ``net.extra`` so that information is not silently lost.
    """

    if "throat.conns" not in network_dict or "pore.coords" not in network_dict:
        if strict:
            raise KeyError(
                "PoreSpy/OpenPNM-style dict must include 'throat.conns' and 'pore.coords'"
            )

    pore_data: dict[str, np.ndarray] = {}
    throat_data: dict[str, np.ndarray] = {}
    pore_labels: dict[str, np.ndarray] = {}
    throat_labels: dict[str, np.ndarray] = {}
    extra: dict[str, object] = {}

    throat_conns = None
    pore_coords = None

    for key, value in network_dict.items():
        arr = _normalize_value(value)
        if key in _PORESPY_KEYMAP:
            family, canonical, reserved = _PORESPY_KEYMAP[key]
            if reserved == "conns":
                throat_conns = np.asarray(arr, dtype=int)
                continue
            if reserved == "coords":
                pore_coords = np.asarray(arr, dtype=float)
                continue
            if canonical is None:
                continue
            if family == "pore":
                pore_data[canonical] = np.asarray(arr)
            else:
                throat_data[canonical] = np.asarray(arr)
            continue

        if key.startswith("pore."):
            sub = key[5:]
            if np.asarray(arr).dtype == bool:
                pore_labels[sub] = np.asarray(arr, dtype=bool)
            else:
                pore_data[sub.replace(".", "_")] = np.asarray(arr)
        elif key.startswith("throat."):
            sub = key[7:]
            if np.asarray(arr).dtype == bool:
                throat_labels[sub] = np.asarray(arr, dtype=bool)
            else:
                throat_data[sub.replace(".", "_")] = np.asarray(arr)
        else:
            extra[key] = value

    if throat_conns is None or pore_coords is None:
        throat_conns = np.asarray(network_dict.get("throat.conns"))
        pore_coords = np.asarray(network_dict.get("pore.coords"), dtype=float)
        if throat_conns.ndim == 0 or pore_coords.ndim == 0:
            raise KeyError("Required keys 'throat.conns' and/or 'pore.coords' missing")

    if pore_coords.ndim == 2 and pore_coords.shape[1] == 2:
        pore_coords = np.column_stack([pore_coords, np.zeros(pore_coords.shape[0])])

    for alias, canonical in _AXIS_LABEL_ALIASES:
        if alias in pore_labels and canonical not in pore_labels:
            pore_labels[canonical] = pore_labels[alias]

    _ensure_inscribed_size_aliases(pore_data)
    _ensure_inscribed_size_aliases(throat_data)

    geometry_repairs = _normalize_geometry_repairs_mode(geometry_repairs)

    if geometry_repairs not in {None, "imperial_export"}:
        raise ValueError("geometry_repairs must be None or 'imperial_export'")
    if geometry_repairs == "imperial_export":
        extra["geometry_repairs"] = _apply_imperial_export_geometry_repairs(
            pore_data,
            throat_data,
            np.asarray(throat_conns, dtype=int),
            num_pores=int(pore_coords.shape[0]),
            random_seed=repair_seed,
        )

    _derive_missing_geometry(pore_data, throat_data)

    if "hydraulic_size_factors" in throat_data:
        extra["throat.hydraulic_size_factors"] = throat_data.pop("hydraulic_size_factors")
        warnings.warn(
            "Stored throat.hydraulic_size_factors in net.extra (no v0.1 solver integration yet)",
            RuntimeWarning,
            stacklevel=2,
        )

    net = Network(
        throat_conns=throat_conns,
        pore_coords=pore_coords,
        sample=sample or SampleGeometry(),
        provenance=provenance or Provenance(source_kind="porespy"),
        pore=pore_data,
        throat=throat_data,
        pore_labels=pore_labels,
        throat_labels=throat_labels,
        extra=extra,
    )
    validate_network(net)
    return net

OpenPNM Interoperability

voids.io.openpnm

from_porespy

from_porespy(
    network_dict,
    *,
    sample=None,
    provenance=None,
    strict=True,
    geometry_repairs=None,
    repair_seed=0,
)

Build a :class:Network from a PoreSpy/OpenPNM-style mapping.

Parameters:

Name Type Description Default
network_dict Mapping[str, object]

Mapping containing PoreSpy/OpenPNM keys such as pore.coords and throat.conns.

required
sample SampleGeometry | None

Sample geometry metadata attached to the resulting network. If omitted, a default empty :class:SampleGeometry is used.

None
provenance Provenance | None

Provenance metadata. If omitted, a default record with source_kind="porespy" is created.

None
strict bool

If True, missing topology keys immediately raise an error.

True
geometry_repairs str | None

Optional extraction-style preprocessing mode. Set to "imperial_export" to apply the Imperial College export heuristics for throat shape-factor repair and pore shape-factor reconstruction. The default None preserves the imported geometry as-is apart from basic alias normalization. The legacy name "pnextract" is still accepted as a deprecated alias for backward compatibility.

None
repair_seed int | None

Seed for any stochastic repair branch. Only used when geometry_repairs="imperial_export".

0

Returns:

Type Description
Network

Imported network in the canonical voids representation.

Raises:

Type Description
KeyError

If the required topology keys are missing.

Notes

The importer performs several normalizations:

  • PoreSpy/OpenPNM aliases are mapped to canonical voids names.
  • Two-dimensional coordinates are embedded into 3D as (x, y, 0).
  • Basic missing geometry is derived when possible.
  • Common boundary aliases such as left and right are mirrored to inlet_xmin and outlet_xmax.

Unsupported nested arrays such as throat.hydraulic_size_factors are preserved in net.extra so that information is not silently lost.

Source code in src/voids/io/porespy.py
def from_porespy(
    network_dict: Mapping[str, object],
    *,
    sample: SampleGeometry | None = None,
    provenance: Provenance | None = None,
    strict: bool = True,
    geometry_repairs: str | None = None,
    repair_seed: int | None = 0,
) -> Network:
    """Build a :class:`Network` from a PoreSpy/OpenPNM-style mapping.

    Parameters
    ----------
    network_dict :
        Mapping containing PoreSpy/OpenPNM keys such as ``pore.coords`` and
        ``throat.conns``.
    sample :
        Sample geometry metadata attached to the resulting network. If omitted,
        a default empty :class:`SampleGeometry` is used.
    provenance :
        Provenance metadata. If omitted, a default record with
        ``source_kind="porespy"`` is created.
    strict :
        If ``True``, missing topology keys immediately raise an error.
    geometry_repairs :
        Optional extraction-style preprocessing mode. Set to
        ``"imperial_export"`` to apply the Imperial College export heuristics
        for throat shape-factor repair and pore shape-factor reconstruction.
        The default ``None`` preserves the imported geometry as-is apart from
        basic alias normalization. The legacy name ``"pnextract"`` is still
        accepted as a deprecated alias for backward compatibility.
    repair_seed :
        Seed for any stochastic repair branch. Only used when
        ``geometry_repairs="imperial_export"``.

    Returns
    -------
    Network
        Imported network in the canonical ``voids`` representation.

    Raises
    ------
    KeyError
        If the required topology keys are missing.

    Notes
    -----
    The importer performs several normalizations:

    - PoreSpy/OpenPNM aliases are mapped to canonical ``voids`` names.
    - Two-dimensional coordinates are embedded into 3D as ``(x, y, 0)``.
    - Basic missing geometry is derived when possible.
    - Common boundary aliases such as ``left`` and ``right`` are mirrored to
      ``inlet_xmin`` and ``outlet_xmax``.

    Unsupported nested arrays such as ``throat.hydraulic_size_factors`` are
    preserved in ``net.extra`` so that information is not silently lost.
    """

    if "throat.conns" not in network_dict or "pore.coords" not in network_dict:
        if strict:
            raise KeyError(
                "PoreSpy/OpenPNM-style dict must include 'throat.conns' and 'pore.coords'"
            )

    pore_data: dict[str, np.ndarray] = {}
    throat_data: dict[str, np.ndarray] = {}
    pore_labels: dict[str, np.ndarray] = {}
    throat_labels: dict[str, np.ndarray] = {}
    extra: dict[str, object] = {}

    throat_conns = None
    pore_coords = None

    for key, value in network_dict.items():
        arr = _normalize_value(value)
        if key in _PORESPY_KEYMAP:
            family, canonical, reserved = _PORESPY_KEYMAP[key]
            if reserved == "conns":
                throat_conns = np.asarray(arr, dtype=int)
                continue
            if reserved == "coords":
                pore_coords = np.asarray(arr, dtype=float)
                continue
            if canonical is None:
                continue
            if family == "pore":
                pore_data[canonical] = np.asarray(arr)
            else:
                throat_data[canonical] = np.asarray(arr)
            continue

        if key.startswith("pore."):
            sub = key[5:]
            if np.asarray(arr).dtype == bool:
                pore_labels[sub] = np.asarray(arr, dtype=bool)
            else:
                pore_data[sub.replace(".", "_")] = np.asarray(arr)
        elif key.startswith("throat."):
            sub = key[7:]
            if np.asarray(arr).dtype == bool:
                throat_labels[sub] = np.asarray(arr, dtype=bool)
            else:
                throat_data[sub.replace(".", "_")] = np.asarray(arr)
        else:
            extra[key] = value

    if throat_conns is None or pore_coords is None:
        throat_conns = np.asarray(network_dict.get("throat.conns"))
        pore_coords = np.asarray(network_dict.get("pore.coords"), dtype=float)
        if throat_conns.ndim == 0 or pore_coords.ndim == 0:
            raise KeyError("Required keys 'throat.conns' and/or 'pore.coords' missing")

    if pore_coords.ndim == 2 and pore_coords.shape[1] == 2:
        pore_coords = np.column_stack([pore_coords, np.zeros(pore_coords.shape[0])])

    for alias, canonical in _AXIS_LABEL_ALIASES:
        if alias in pore_labels and canonical not in pore_labels:
            pore_labels[canonical] = pore_labels[alias]

    _ensure_inscribed_size_aliases(pore_data)
    _ensure_inscribed_size_aliases(throat_data)

    geometry_repairs = _normalize_geometry_repairs_mode(geometry_repairs)

    if geometry_repairs not in {None, "imperial_export"}:
        raise ValueError("geometry_repairs must be None or 'imperial_export'")
    if geometry_repairs == "imperial_export":
        extra["geometry_repairs"] = _apply_imperial_export_geometry_repairs(
            pore_data,
            throat_data,
            np.asarray(throat_conns, dtype=int),
            num_pores=int(pore_coords.shape[0]),
            random_seed=repair_seed,
        )

    _derive_missing_geometry(pore_data, throat_data)

    if "hydraulic_size_factors" in throat_data:
        extra["throat.hydraulic_size_factors"] = throat_data.pop("hydraulic_size_factors")
        warnings.warn(
            "Stored throat.hydraulic_size_factors in net.extra (no v0.1 solver integration yet)",
            RuntimeWarning,
            stacklevel=2,
        )

    net = Network(
        throat_conns=throat_conns,
        pore_coords=pore_coords,
        sample=sample or SampleGeometry(),
        provenance=provenance or Provenance(source_kind="porespy"),
        pore=pore_data,
        throat=throat_data,
        pore_labels=pore_labels,
        throat_labels=throat_labels,
        extra=extra,
    )
    validate_network(net)
    return net

to_openpnm_dict

to_openpnm_dict(net, *, include_extra=False)

Export a network to an OpenPNM/PoreSpy-style flat dictionary.

Parameters:

Name Type Description Default
net Network

Network to export.

required
include_extra bool

If True, merge net.extra into the output dictionary.

False

Returns:

Type Description
dict of str to Any

Flat mapping using keys such as "pore.coords" and "throat.conns".

Notes

The mapping preserves aliases expected by :func:voids.io.porespy.from_porespy. Conduit-length fields are emitted both under their internal names (for round-tripping within voids) and under OpenPNM-style names such as throat.conduit_lengths.pore1.

Source code in src/voids/io/openpnm.py
def to_openpnm_dict(net: Network, *, include_extra: bool = False) -> dict[str, Any]:
    """Export a network to an OpenPNM/PoreSpy-style flat dictionary.

    Parameters
    ----------
    net :
        Network to export.
    include_extra :
        If ``True``, merge ``net.extra`` into the output dictionary.

    Returns
    -------
    dict of str to Any
        Flat mapping using keys such as ``"pore.coords"`` and ``"throat.conns"``.

    Notes
    -----
    The mapping preserves aliases expected by :func:`voids.io.porespy.from_porespy`.
    Conduit-length fields are emitted both under their internal names
    (for round-tripping within ``voids``) and under OpenPNM-style names
    such as ``throat.conduit_lengths.pore1``.
    """

    out: dict[str, Any] = {
        "pore.coords": np.asarray(net.pore_coords, dtype=float).copy(),
        "throat.conns": np.asarray(net.throat_conns, dtype=int).copy(),
    }
    for k, v in net.pore.items():
        out[f"pore.{k}"] = np.asarray(v).copy()
    for k, v in net.throat.items():
        if k in {"pore1_length", "core_length", "pore2_length"}:
            alias_map = {
                "pore1_length": "throat.conduit_lengths.pore1",
                "core_length": "throat.conduit_lengths.throat",
                "pore2_length": "throat.conduit_lengths.pore2",
            }
            out[alias_map[k]] = np.asarray(v).copy()
        out[f"throat.{k}"] = np.asarray(v).copy()
    for k, v in net.pore_labels.items():
        out[f"pore.{k}"] = np.asarray(v, dtype=bool).copy()
    for k, v in net.throat_labels.items():
        out[f"throat.{k}"] = np.asarray(v, dtype=bool).copy()
    if include_extra:
        out.update(net.extra)
    return out

to_openpnm_network

to_openpnm_network(
    net,
    *,
    copy_properties=True,
    copy_labels=True,
    include_extra=False,
)

Convert a :class:Network into an OpenPNM network object.

Parameters:

Name Type Description Default
net Network

Network to convert.

required
copy_properties bool

If True, copy numeric pore and throat properties into the OpenPNM object.

True
copy_labels bool

If True, copy pore and throat boolean labels.

True
include_extra bool

If True, attempt to copy entries from net.extra whose keys already follow the pore.* or throat.* naming convention.

False

Returns:

Type Description
Any

OpenPNM network object. The precise class depends on the installed OpenPNM version.

Raises:

Type Description
ImportError

If OpenPNM is not installed.

RuntimeError

If a compatible OpenPNM network object cannot be instantiated.

Notes

OpenPNM's constructor signatures vary across versions. This helper tries a small set of known call patterns and always assigns pore.coords and throat.conns explicitly afterward so that topology transfer is version-robust.

Source code in src/voids/io/openpnm.py
def to_openpnm_network(
    net: Network,
    *,
    copy_properties: bool = True,
    copy_labels: bool = True,
    include_extra: bool = False,
) -> openpnm.network.Network:
    """Convert a :class:`Network` into an OpenPNM network object.

    Parameters
    ----------
    net :
        Network to convert.
    copy_properties :
        If ``True``, copy numeric pore and throat properties into the OpenPNM object.
    copy_labels :
        If ``True``, copy pore and throat boolean labels.
    include_extra :
        If ``True``, attempt to copy entries from ``net.extra`` whose keys already
        follow the ``pore.*`` or ``throat.*`` naming convention.

    Returns
    -------
    Any
        OpenPNM network object. The precise class depends on the installed OpenPNM version.

    Raises
    ------
    ImportError
        If OpenPNM is not installed.
    RuntimeError
        If a compatible OpenPNM network object cannot be instantiated.

    Notes
    -----
    OpenPNM's constructor signatures vary across versions. This helper tries a small
    set of known call patterns and always assigns ``pore.coords`` and
    ``throat.conns`` explicitly afterward so that topology transfer is version-robust.
    """

    try:
        import openpnm as op
    except Exception as exc:  # pragma: no cover - optional dependency
        raise ImportError("OpenPNM is not installed") from exc

    coords = np.asarray(net.pore_coords, dtype=float)
    conns = np.asarray(net.throat_conns, dtype=int)

    pn = None
    errs: list[Exception] = []
    for ctor in (
        lambda: op.network.Network(coords=coords, conns=conns),
        lambda: op.network.Network(conns=conns, coords=coords),
        lambda: op.network.Network(),
    ):
        try:
            pn = ctor()
            break
        except Exception as e:  # pragma: no cover - depends on OpenPNM version
            errs.append(e)
    if pn is None:  # pragma: no cover
        raise RuntimeError(f"Unable to instantiate OpenPNM Network: {errs!r}")

    pn["pore.coords"] = coords
    pn["throat.conns"] = conns

    if copy_properties:
        for k, v in net.pore.items():
            pn[f"pore.{k}"] = np.asarray(v)
        for k, v in net.throat.items():
            pn[f"throat.{k}"] = np.asarray(v)
    if copy_labels:
        for k, v in net.pore_labels.items():
            pn[f"pore.{k}"] = np.asarray(v, dtype=bool)
        for k, v in net.throat_labels.items():
            pn[f"throat.{k}"] = np.asarray(v, dtype=bool)
    if include_extra:
        for k, v in net.extra.items():
            if isinstance(k, str) and (k.startswith("pore.") or k.startswith("throat.")):
                try:
                    pn[k] = np.asarray(v)
                except Exception:
                    pass
    return pn