Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion blobid/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .solver import get_labels
from . import labeling
from . import plic

__all__ = ['get_labels', 'labeling']
__all__ = ['get_labels', 'labeling', 'plic']
8 changes: 8 additions & 0 deletions blobid/labeling/labeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def apply_ccl(
labels : ndarray[ni, nj, nk]
An array of type `label_type` with the same shape as `is_object`.

Notes
-----
The labeling algorithm used is based on work by He, Chao & Suzuki.[1]_


.. [1] L. He, Y. Chao & K. Suzuki, "A Linear-Time Two-Scan Labeling Algorithm," IEEE International Conference
on Image Processing, 2007. [10.1109/ICIP.2007.4379810](https://doi.org/10.1109/ICIP.2007.4379810)

"""
# checks
assert is_object.ndim == 3
Expand Down
8 changes: 8 additions & 0 deletions blobid/plic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
r"""
Implements piecewise linear interface calculation (PLIC), where the interface in each cell can be represented by
:math:`\vec{n}\cdot\vec{x}=\alpha`
"""

from .plic import get_normals, SUPPORTED_METHODS

__all__ = ['get_normals', 'SUPPORTED_METHODS']
19 changes: 19 additions & 0 deletions blobid/plic/central_difference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Implementation of simple central differencing for interface reconstruction
"""
import numpy as np
import numpy.typing as npt


def get_normals(f: np.ndarray, normals_dtype: npt.DTypeLike) -> np.ndarray:
"""
Calculate the sign of the interface normal using central difference
"""

n = np.empty((3, f.shape[0]-2, f.shape[1]-2, f.shape[2]-2), dtype=normals_dtype)

n[0] = np.sign(f[:-2, 1:-1, 1:-1] - f[2:, 1:-1, 1:-1])
n[1] = np.sign(f[1:-1, :-2, 1:-1] - f[1:-1, 2:, 1:-1])
n[2] = np.sign(f[1:-1, 1:-1, :-2] - f[1:-1, 1:-1, 2:])

return n
47 changes: 15 additions & 32 deletions blobid/reconstruction/reconstruction.py → blobid/plic/nivira.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,25 @@
"""
Calculate the sign of the interface normal based on a 3x3(x3) stencil
Implementation of No Inversion VOF Interface Reconstruction Algorithm (NIVIRA)

Reference
---------

G. D. Weymouth & D. K.-P. Yue, "Conservative Volume-of-Fluid method for free-surface simulations on
Cartesian-grids," Journal of Computational Physics, vol. 229, pp. 2853--2865, 2010.
[10.1016/j.jcp.2009.12.018](https://doi.org/10.1016/j.jcp.2009.12.018)
"""
import warnings
import numpy as np
import numpy.typing as npt

from .._numba_support import njit, numba_availible

NORMALS_TYPE = np.int8


def normals(f: np.ndarray, normals_method: str) -> np.ndarray:
"""
Calculate the sign of the interface normals using method specified by normals_method
"""
match normals_method:
case 'CD':
return normals_CD(f)
case 'WY':
return normals_WY(f)
case _:
raise ValueError(f"normals_method '{normals_method}' is not supported")


def normals_CD(f: np.ndarray) -> np.ndarray:
def get_normals(f: np.ndarray, normals_dtype: npt.DTypeLike) -> np.ndarray:
"""
Calculate the sign of the interface normal using central difference
Calculate the sign of the interface normal using NIVIRA
"""

n = np.empty((3, f.shape[0]-2, f.shape[1]-2, f.shape[2]-2), dtype=NORMALS_TYPE)

n[0] = np.sign(f[:-2, 1:-1, 1:-1] - f[2:, 1:-1, 1:-1])
n[1] = np.sign(f[1:-1, :-2, 1:-1] - f[1:-1, 2:, 1:-1])
n[2] = np.sign(f[1:-1, 1:-1, :-2] - f[1:-1, 1:-1, 2:])

return n


def normals_WY(f: np.ndarray) -> np.ndarray:
# Numba only supports float32 and float64
if numba_availible and (f.dtype not in (np.float32, np.float64)):
warnings.warn(f"void_fraction type {f.dtype.name} not supported, using float32")
Expand All @@ -46,10 +29,10 @@ def normals_WY(f: np.ndarray) -> np.ndarray:
d_dom = _get_dominant_direction(f)

# calculate normals using 3x3x3 stencil described by Weymouth and Yue
n = np.empty((3, f.shape[0]-2, f.shape[1]-2, f.shape[2]-2), dtype=NORMALS_TYPE)
n = np.empty((3, f.shape[0]-2, f.shape[1]-2, f.shape[2]-2), dtype=normals_dtype)

for d in range(3):
n[d] = _normals_WY(f, d_dom, d)
n[d] = _normals_WY(f, d_dom, d, normals_dtype)

return n

Expand All @@ -71,8 +54,8 @@ def _get_dominant_direction(f):


@njit
def _normals_WY(f, d_dom, d) -> np.ndarray:
n = np.zeros(d_dom.shape, dtype=NORMALS_TYPE)
def _normals_WY(f, d_dom, d, dtype) -> np.ndarray:
n = np.zeros(d_dom.shape, dtype=dtype)

for i, j, k in np.argwhere(d_dom != -1):
if d_dom[i, j, k] == d:
Expand Down
64 changes: 64 additions & 0 deletions blobid/plic/plic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
Calculate the sign of the interface normal based on a 3x3(x3) stencil
"""
import numpy as np

from .central_difference import get_normals as get_normals_CD
from .nivira import get_normals as get_normals_WY

NORMALS_TYPE = np.int8

SUPPORTED_METHODS = ['CD', 'WY']
"""Supported methods for calculating interface normals"""


def get_normals(void_fraction: np.ndarray, normals_method: str = 'CD') -> np.ndarray:
r"""
Calculate the sign of the interface normals using method specified by `normals_method`

Parameters
----------
void_fraction : ndarray[ni+2, nj+2, nk+2]
The void fractions for each grid cell.
normals_method: {'CD', 'WY'}, optional
Method used to calculate interface normals. Defaults to 'CD'.

Returns
-------
normals : ndarray[3, ni, nj, nk]
An integer array which returns the sign of the interface normal.
If the interface in a grid cell is undefined, :math:`\langle 0, 0, 0\rangle` will be returned.

Notes
-----

The signs of the interface normal :math:`\langle n_x, n_y, n_z\rangle` are calculated using the method set by
`normals_method`:

- If `normals_method` is `CD`, central differencing is used. For example
.. math::
\mathrm{sign}(n_x) = - \mathrm{sign}(f_{i+1} - f_{i-1})

- If `normals_method` is `WY`, normals are calculated using the No Inversion VOF Interface Reconstruction Algorithm
(NIVIRA) described by Weymouth and Yue.[1]_ `WY` is more accurate than `CD` but can be slower
(especially if not using Numba).

For simplicity, all methods assume constant grid spacing.


.. [1] G. D. Weymouth & D. K.-P. Yue, "Conservative Volume-of-Fluid method for free-surface simulations on
Cartesian-grids," Journal of Computational Physics, vol. 229, pp. 2853--2865, 2010.
[10.1016/j.jcp.2009.12.018](https://doi.org/10.1016/j.jcp.2009.12.018)

"""
# checks
assert void_fraction.ndim == 3
assert all(dim > 2 for dim in void_fraction.shape)

match normals_method:
case 'CD':
return get_normals_CD(void_fraction, NORMALS_TYPE)
case 'WY':
return get_normals_WY(void_fraction, NORMALS_TYPE)
case _:
raise ValueError(f"normals_method '{normals_method}' is not supported")
3 changes: 0 additions & 3 deletions blobid/reconstruction/__init__.py

This file was deleted.

27 changes: 7 additions & 20 deletions blobid/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ._domain import VOFDomain

from . import labeling
from . import reconstruction
from . import plic


def get_labels(
Expand Down Expand Up @@ -37,7 +37,8 @@ def get_labels(
use_normals : bool, optional
Whether to use interface normals to determine connectedness. Defaults to True.
normals_method: {'CD', 'WY'}, optional
Method used to calculate interface normals. Defaults to 'CD'.
Passed to [plic.get_normals()](blobid/plic.html#get_normals) for calculating interface
normals.
label_type : dtype, optional
Determines the integer data-type of `labels`. Defaults to `numpy.uint32`

Expand Down Expand Up @@ -79,20 +80,13 @@ def get_labels(
In 3-D, each cell has 6 neighbor cells it can be connected to.
The first requirement for two neighbor cells to be connected is that they are both object cells.
In addition (unless `use_normals` is False), the interface normals of each cell must not be opposed.[2]_

The sign of the interface normal :math:`\langle n_x, n_y, n_z\rangle` using the method set by `normals_method`.

- If `normals_method` is `CD`, central differencing is used. For example
.. math::
\mathrm{sign}(n_x) = - \mathrm{sign}(f_{i+1} - f_{i-1})

- If `normals_method` is `WY`, normals are calculated based on a 3x3x3 stencil using a second-order interface
reconstruction method.[3]_
Interface normals are calculated using the method set by `normals_method`
(see [reconstruction.get_normals()](blobid/reconstruction.html#get_normals)).

Labeling
--------
Based on this object and connectivity criteria, labels are assigned to each blob (defined as a connected set
of object cells) with a two-scan labeling algorithm.[4]_
of object cells) with a two-scan labeling algorithm.[3]_

Examples
========
Expand Down Expand Up @@ -136,13 +130,6 @@ def get_labels(
of connected components with volume-of-fluid method," Computers & Fluids, vol. 197, pp. 104373, 2020.
[10.1016/j.compfluid.2019.104373](https://doi.org/10.1016/j.compfluid.2019.104373)

.. [3] G. D. Weymouth & D. K.-P. Yue, "Conservative Volume-of-Fluid method for free-surface simulations on
Cartesian-grids," Journal of Computational Physics, vol. 229, pp. 2853--2865, 2010.
[10.1016/j.jcp.2009.12.018](https://doi.org/10.1016/j.jcp.2009.12.018)

.. [4] L. He, Y. Chao & K. Suzuki, "A Linear-Time Two-Scan Labeling Algorithm," IEEE International Conference
on Image Processing, 2007. [10.1109/ICIP.2007.4379810](https://doi.org/10.1109/ICIP.2007.4379810)

"""

# Defaults
Expand All @@ -160,7 +147,7 @@ def get_labels(

# calculate connectivity
if use_normals:
normals = reconstruction.normals(domain.vof(padding=1), normals_method)
normals = plic.get_normals(domain.vof(padding=1), normals_method)
else:
normals = None

Expand Down
43 changes: 43 additions & 0 deletions tests/plic/test_get_normals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import hashlib

import numpy as np
import blobid.plic as plic


def test_undefined_normals():
"""Tests where normal should be all zeros (undefined)"""

def assert_undefined(norm):
n = norm.squeeze()
assert n[0] == 0
assert n[1] == 0
assert n[2] == 0

# all zeros
f = np.zeros((3, 3, 3))
for method in plic.SUPPORTED_METHODS:
assert_undefined(plic.get_normals(f, method))

# all ones
f = np.ones((3, 3, 3))
for method in plic.SUPPORTED_METHODS:
assert_undefined(plic.get_normals(f, method))

# any value in a range between 0 and 1
for _ in range(1000):
f = np.ones((3, 3, 3)) * np.random.rand()
for method in plic.SUPPORTED_METHODS:
assert_undefined(plic.get_normals(f, method))


def test_end_to_end(fs_vof):
expected_result = [
['CD', 'c90626929711b744621692f3561700a1'],
['WY', '9ac131e45e11e9f9457327c01ad3fb8e']
]

for method, signature in expected_result:
normals = plic.get_normals(fs_vof.astype(np.float32), method)

hash_object = hashlib.md5(normals.tobytes(order='C'))
assert hash_object.hexdigest() == signature
40 changes: 40 additions & 0 deletions tests/plic/test_nivira.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import warnings

import pytest
import numpy as np
import blobid.plic.nivira as nivira

from blobid._numba_support import numba_availible
from blobid.plic.plic import NORMALS_TYPE


def test_WY_Fig2():
"""Figure 2 from Weymouth and Yue"""

f = np.empty((3, 3, 3))
for k in range(3):
f[:, :, k] = np.array([[1.0, 1.0, 0.95],
[1.0, 0.9, 0.3],
[0.7, 0.15, 0.0]])

assert nivira._get_dominant_direction(f)[0, 0, 0] == 0

n = nivira.get_normals(f, NORMALS_TYPE).squeeze()
assert n[0] > 0
assert n[1] > 0
assert n[2] == 0


def test_vof_type():
f = np.random.rand(3, 3, 3)

# float16 should give a warning
if numba_availible:
with pytest.warns():
nivira.get_normals(f.astype(np.float16), NORMALS_TYPE)

# float32 and float64 should work without warning
with warnings.catch_warnings():
warnings.simplefilter("error")
nivira.get_normals(f.astype(np.float32), NORMALS_TYPE)
nivira.get_normals(f.astype(np.float64), NORMALS_TYPE)
Loading