Skip to content

liblaf.melon.tet ยค

Functions:

  • cell_data_to_point_data โ€“
  • compute_volume_fraction โ€“
  • extract_cells โ€“
  • fix_winding โ€“
  • flip โ€“
  • point_data_to_cell_data โ€“

cell_data_to_point_data ยค

cell_data_to_point_data(
    mesh: UnstructuredGrid,
    data: str | Iterable[str] | None = None,
) -> UnstructuredGrid
Source code in src/liblaf/melon/tet/_transform_data.py
 8
 9
10
11
12
13
14
15
16
17
18
def cell_data_to_point_data(
    mesh: pv.UnstructuredGrid, data: str | Iterable[str] | None = None
) -> pv.UnstructuredGrid:
    data: Collection[str] = utils.as_names(data, mesh.cell_data)
    source: pv.UnstructuredGrid = pv.UnstructuredGrid()
    source.copy_structure(mesh)
    source.cell_data.update({name: mesh.cell_data[name] for name in data}, copy=False)
    output: pv.UnstructuredGrid = source.cell_data_to_point_data()  # pyright: ignore[reportAssignmentType]
    for name in data:
        mesh.point_data[name] = output.point_data[name]
    return mesh

compute_volume_fraction ยค

compute_volume_fraction(
    mesh: TetMeshLike,
    surface: TriMeshLike,
    *,
    chunk_size: int = 10**6,
    n_samples: int = 1000,
) -> Float[Array, " cells"]
Source code in src/liblaf/melon/tet/_volume_fraction.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def compute_volume_fraction(
    mesh: TetMeshLike,
    surface: TriMeshLike,
    *,
    chunk_size: int = 10**6,
    n_samples: int = 1000,
) -> Float[Array, " cells"]:
    from liblaf.melon import io, tri
    from liblaf.melon.barycentric import (
        barycentric_to_points,
        sample_barycentric_coords,
    )

    mesh: pv.UnstructuredGrid = io.as_unstructured_grid(mesh)
    surface: pv.PolyData = io.as_polydata(surface)
    points: Float[Array, "points 3"] = jnp.asarray(mesh.points)
    cells: Integer[Array, "cells 4"] = jnp.asarray(mesh.cells_dict[pv.CellType.TETRA])  # pyright: ignore[reportArgumentType]
    volume_fraction: Float[Array, " cells"] = jnp.zeros((mesh.n_cells,))
    query = tri.MeshQuery(surface)

    # 1. AABB check
    # check if the AABB of the tetra intersects the AABB of the surface
    cell_points: Float[Array, "cells 4 3"] = points[cells]
    cell_bound_min: Float[Array, "cells 3"] = jnp.min(cell_points, axis=1)
    cell_bound_max: Float[Array, "cells 3"] = jnp.max(cell_points, axis=1)
    in_aabb: Bool[Array, " cells"] = jnp.all(
        (query.bounds[0] <= cell_bound_max) & (cell_bound_min <= query.bounds[1]),
        axis=-1,
    )

    # 2. SDF check
    # use Signed Distance Function to classify tetras as fully inside, fully outside, or crossing
    candidates: Integer[Array, " candidates"] = jnp.flatnonzero(in_aabb)
    if candidates.size == 0:
        return volume_fraction
    candidate_cell_points: Float[Array, "candidates 4 3"] = cell_points[candidates]
    centroids: Float[Array, "candidates 3"] = jnp.mean(candidate_cell_points, axis=1)
    # maximum distance from centroid to vertices
    radius: Float[Array, " candidates"] = jnp.max(
        jnp.linalg.norm(candidate_cell_points - centroids[:, jnp.newaxis, :], axis=-1),
        axis=-1,
    )
    sdf: Float[Array, " candidates"] = query.signed_distance(centroids)
    fully_inside: Bool[Array, " candidates"] = sdf < -radius
    fully_outside: Bool[Array, " candidates"] = sdf > radius
    volume_fraction = volume_fraction.at[candidates[fully_inside]].set(1.0)

    # 3. sampling
    candidates: Integer[Array, " candidates"] = candidates[
        ~(fully_inside | fully_outside)
    ]
    if candidates.size == 0:
        return volume_fraction
    for chunk in jnp.array_split(
        candidates, math.ceil(candidates.size * n_samples / chunk_size)
    ):
        barycentric: Float[Array, "cells samples 4"] = sample_barycentric_coords(
            (chunk.size, n_samples, 4)
        )
        samples: Float[Array, "cells samples 3"] = barycentric_to_points(
            points[cells[chunk]][:, jnp.newaxis, :, :], barycentric
        )
        contains: Bool[Array, "cells samples"] = query.contains(
            samples.reshape(chunk.size * n_samples, 3)
        ).reshape(chunk.size, n_samples)
        fraction: Float[Array, " cells"] = (
            jnp.count_nonzero(contains, axis=-1) / n_samples
        )
        volume_fraction = volume_fraction.at[chunk].set(fraction)
    return volume_fraction

extract_cells ยค

extract_cells(
    mesh: Any,
    ind: int | VectorLike[int] | VectorLike[bool],
    *,
    invert: bool = False,
) -> UnstructuredGrid
Source code in src/liblaf/melon/tet/_extract.py
10
11
12
13
14
15
16
17
18
def extract_cells(
    mesh: Any, ind: int | VectorLike[int] | VectorLike[bool], *, invert: bool = False
) -> pv.UnstructuredGrid:
    mesh: pv.UnstructuredGrid = io.as_unstructured_grid(mesh)
    ind = np.asarray(ind)
    if np.isdtype(ind.dtype, "bool"):
        ind = np.flatnonzero(ind)
    result: pv.UnstructuredGrid = mesh.extract_cells(ind, invert=invert)  # pyright: ignore[reportAssignmentType]
    return result

fix_winding ยค

fix_winding(
    mesh: Any, *, check: bool = True
) -> UnstructuredGrid
Source code in src/liblaf/melon/tet/_flip.py
10
11
12
13
14
15
16
17
18
19
def fix_winding(mesh: Any, *, check: bool = True) -> pv.UnstructuredGrid:
    mesh: pv.UnstructuredGrid = io.as_unstructured_grid(mesh)
    mesh = mesh.compute_cell_sizes(length=False, area=False, volume=True)  # pyright: ignore[reportAssignmentType]
    flip_mask: Bool[np.ndarray, " C"] = mesh.cell_data["Volume"] < 0
    if np.any(flip_mask):
        mesh = flip(mesh, flip_mask)
        mesh = mesh.compute_cell_sizes(length=False, area=False, volume=True)  # pyright: ignore[reportAssignmentType]
        if check:
            assert np.all(mesh.cell_data["Volume"] >= 0)
    return mesh

flip ยค

flip(
    mesh: Any, mask: Bool[ArrayLike, " C"]
) -> UnstructuredGrid
Source code in src/liblaf/melon/tet/_flip.py
22
23
24
25
26
27
28
29
30
31
32
33
def flip(mesh: Any, mask: Bool[ArrayLike, " C"]) -> pv.UnstructuredGrid:
    mesh: pv.UnstructuredGrid = io.as_unstructured_grid(mesh)
    mask: Bool[np.ndarray, " C"] = np.asarray(mask)
    if not np.any(mask):
        return mesh
    tetras: Integer[np.ndarray, "C 4"] = mesh.cells_dict[pv.CellType.TETRA]  # pyright: ignore[reportArgumentType]
    # ref: <https://felupe.readthedocs.io/en/latest/felupe/mesh.html#felupe.mesh.flip>
    faces: Integer[np.ndarray, " 3"] = np.asarray([0, 1, 2], np.int32)
    tetras[np.ix_(mask, faces)] = tetras[np.ix_(mask, faces[::-1])]
    result = pv.UnstructuredGrid({pv.CellType.TETRA: tetras}, mesh.points)
    result.copy_attributes(mesh)
    return result

point_data_to_cell_data ยค

point_data_to_cell_data(
    mesh: UnstructuredGrid,
    data: str | Iterable[str] | None = None,
) -> UnstructuredGrid
Source code in src/liblaf/melon/tet/_transform_data.py
21
22
23
24
25
26
27
28
29
30
31
def point_data_to_cell_data(
    mesh: pv.UnstructuredGrid, data: str | Iterable[str] | None = None
) -> pv.UnstructuredGrid:
    data: Collection[str] = utils.as_names(data, mesh.point_data)
    source: pv.UnstructuredGrid = pv.UnstructuredGrid()
    source.copy_structure(mesh)
    source.point_data.update({name: mesh.point_data[name] for name in data}, copy=False)
    output: pv.UnstructuredGrid = source.point_data_to_cell_data()  # pyright: ignore[reportAssignmentType]
    for name in data:
        mesh.cell_data[name] = output.cell_data[name]
    return mesh