Source code for cfdmod.io.mesh

"""Multi-format mesh loader.

The v3 pressure pipeline ops (``mesh_attach``, ``body_grouping``) accept
the mesh as a file path resolved against the template's root. This
helper handles the path dispatch by suffix:

- ``.lnas`` -> :func:`lnas.LnasFormat.from_file` (preserves authored surfaces)
- ``.stl``  -> :func:`lnas.LnasFormat.from_stl` (single ``"all"`` surface)
- ``.h5``   -> read ``/Triangles + /Geometry``, single ``"all"`` surface
- ``.xdmf`` -> redirect to the sibling ``.h5`` (XDMF is metadata only)

Files that already have authored surface labels (``.lnas``) keep them.
The other formats produce one synthetic ``"all"`` surface covering
every triangle, so YAML templates targeting that surface (or
``surfaces: []`` for "use everything") work uniformly.
"""

from __future__ import annotations

__all__ = ["load_mesh", "mesh_from_h5"]

import pathlib

import h5py
import numpy as np
from lnas import LnasFormat, LnasGeometry


def _resolve_lnas_version() -> str:
    """Pick a version string the installed lnas reader will accept.

    lnas exposes ``_CURRENT_VERSION`` (e.g. ``"v0.5.2"``) on
    :mod:`lnas.fmt`; we use that when available so cfdmod tracks the
    library it's pinned against. If the symbol moves or disappears, fall
    back to a hard-coded value compatible with lnas's major-version check.
    """
    try:
        from lnas import fmt as _lnas_fmt

        version = getattr(_lnas_fmt, "_CURRENT_VERSION", None)
        if isinstance(version, str) and version:
            return version
    except Exception:
        pass
    return "v0.5.2"


_LNAS_VERSION = _resolve_lnas_version()
_DEFAULT_SURFACE_NAME = "all"


[docs] def mesh_from_h5(h5_path: pathlib.Path) -> LnasFormat: """Build an :class:`LnasFormat` from an HDF5 file's ``/Triangles + /Geometry`` datasets, with one synthetic surface covering every triangle. Used both by :func:`load_mesh` (for ``.h5``/``.xdmf`` inputs) and as the fallback when a pipeline call omits ``mesh_path`` and the geometry has to come straight from the body or cp timeseries file. """ with h5py.File(h5_path, "r") as f: if "Triangles" not in f or "Geometry" not in f: raise ValueError( f"{h5_path} has no /Triangles + /Geometry; cannot build a mesh from it." ) triangles = f["Triangles"][:].astype(np.int32) vertices = f["Geometry"][:].astype(np.float64) geom = LnasGeometry(vertices=vertices, triangles=triangles) surfaces = {_DEFAULT_SURFACE_NAME: np.arange(len(triangles), dtype=np.int32)} return LnasFormat(version=_LNAS_VERSION, geometry=geom, surfaces=surfaces)
[docs] def load_mesh(source: pathlib.Path | LnasFormat) -> LnasFormat: """Resolve a mesh from any supported source. Args: source: Either an :class:`LnasFormat` (returned as-is) or a path ending in ``.lnas``, ``.stl``, ``.h5``, or ``.xdmf``. Returns: LnasFormat ready to drive the pressure pipeline. """ if isinstance(source, LnasFormat): return source path = pathlib.Path(source) suffix = path.suffix.lower() if suffix == ".lnas": return LnasFormat.from_file(path) if suffix == ".stl": return LnasFormat.from_stl(path) if suffix == ".xdmf": h5_sibling = path.with_suffix(".h5") if not h5_sibling.exists(): raise FileNotFoundError( f"{path} references mesh data but the sibling H5 {h5_sibling} is missing." ) return mesh_from_h5(h5_sibling) if suffix == ".h5": return mesh_from_h5(path) raise ValueError( f"Unsupported mesh format {suffix!r} for {path}. Expected one of: .lnas, .stl, .h5, .xdmf." )