Skip to content
Snippets Groups Projects
stencils.py 9.79 KiB


def getStencil(name, ordering='walberla'):
    """
    Stencils are tuples of discrete velocities. They are commonly labeled in the 'DxQy' notation, where d is the
    dimension (length of the velocity tuples) and y is number of discrete velocities.

    :param name: DxQy notation
    :param ordering: the LBM literature does not use a common order of the discrete velocities, therefore here
                     different common orderings are available. All orderings lead to the same method, it just has
                     to be used consistently. Here more orderings are available to compare intermediate results with
                     the literature.
    """
    try:
        return getStencil.data[ordering.lower()][name.upper()]
    except KeyError:
        errMsg = ""
        for orderingName, stencils in getStencil.data.items():
            errMsg += "  %s: %s\n" % (orderingName, ", ".join(stencils.keys()))

        raise ValueError("No such stencil available. Available stencils: <orderingName>( <stencilNames> )\n" + errMsg)
getStencil.data = {

    'walberla': {
        'D2Q9': ((0, 0),
                 (0, 1), (0, -1), (-1, 0), (1, 0),
                 (-1, 1), (1, 1), (-1, -1), (1, -1),),
        'D3Q15': ((0, 0, 0),
                  (0, 1, 0), (0, -1, 0), (-1, 0, 0), (1, 0, 0), (0, 0, 1), (0, 0, -1),
                  (1, 1, 1), (-1, 1, 1), (1, -1, 1), (-1, -1, 1), (1, 1, -1), (-1, 1, -1), (1, -1, -1), (-1, -1, -1)),
        'D3Q19': ((0, 0, 0),
                  (0, 1, 0), (0, -1, 0), (-1, 0, 0), (1, 0, 0), (0, 0, 1), (0, 0, -1),
                  (-1, 1, 0), (1, 1, 0), (-1, -1, 0), (1, -1, 0),
                  (0, 1, 1), (0, -1, 1), (-1, 0, 1), (1, 0, 1),
                  (0, 1, -1), (0, -1, -1), (-1, 0, -1), (1, 0, -1)),
        'D3Q27': ((0, 0, 0),
                  (0, 1, 0), (0, -1, 0), (-1, 0, 0), (1, 0, 0), (0, 0, 1), (0, 0, -1),
                  (-1, 1, 0), (1, 1, 0), (-1, -1, 0), (1, -1, 0),
                  (0, 1, 1), (0, -1, 1), (-1, 0, 1), (1, 0, 1),
                  (0, 1, -1), (0, -1, -1), (-1, 0, -1), (1, 0, -1),
                  (1, 1, 1), (-1, 1, 1), (1, -1, 1), (-1, -1, 1), (1, 1, -1), (-1, 1, -1), (1, -1, -1), (-1, -1, -1))
    },

    'standard': {
        'D2Q9': ((0, 0),
                 (1, 0), (0, 1), (-1, 0), (0, -1),
                 (1, 1), (-1, 1), (-1, -1), (1, -1)),
    },

    'braunschweig': {
        'D2Q9': ((0, 0),
                 (-1, 1), (-1, 0), (-1, -1), (0, -1),
                 (1, -1), (1, 0), (1, 1), (0, 1)),
        'D3Q19': ((0, 0, 0),

                  (1, 0, 0), (-1, 0, 0),
                  (0, 1, 0), (0, -1, 0),
                  (0, 0, 1), (0, 0, -1),

                  (1, 1, 0), (-1, -1, 0),
                  (1, -1, 0), (-1, 1, 0),
                  (1, 0, 1), (-1, 0, -1),
                  (1, 0, -1), (-1, 0, 1),
                  (0, 1, 1), (0, -1, -1),
                  (0, 1, -1), (0, -1, 1)),
    },
    'premnath': {
        'D3Q27': ((0, 0, 0),
                  (1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1),
                  (1, 1, 0), (-1, 1, 0), (1, -1, 0), (-1, -1, 0),
                  (1, 0, 1), (-1, 0, 1), (1, 0, -1), (-1, 0, -1),
                  (0, 1, 1), (0, -1, 1), (0, 1, -1), (0, -1, -1),
                  (1, 1, 1), (-1, 1, 1), (1, -1, 1), (-1, -1, 1),
                  (1, 1, -1), (-1, 1, -1), (1, -1, -1), (-1, -1, -1)),
    }
}


def inverseDirection(direction):
    """Returns inverse i.e. negative of given direction tuple"""
    return tuple([-i for i in direction])


def isValidStencil(stencil, maxNeighborhood=None):
    """
    Tests if a nested sequence is a valid stencil i.e. all the inner sequences have the same length.
    If maxNeighborhood is specified, it is also verified that the stencil does not contain any direction components
    with absolute value greater than the maximal neighborhood.
    """
    expectedDim = len(stencil[0])
    for d in stencil:
        if len(d) != expectedDim:
            return False
        if maxNeighborhood is not None:
            for d_i in d:
                if abs(d_i) > maxNeighborhood:
                    return False
    return True


def isSymmetricStencil(stencil):
    """Tests for every direction d, that -d is also in the stencil"""
    for d in stencil:
        if inverseDirection(d) not in stencil:
            return False
    return True


def stencilsHaveSameEntries(s1, s2):
    if len(s1) != len(s2):
        return False
    return len(set(s1) - set(s2)) == 0


# -------------------------------------- Visualization -----------------------------------------------------------------

def visualizeStencil(stencil, **kwargs):
    dim = len(stencil[0])
    if dim == 2:
        visualizeStencil2D(stencil, **kwargs)
    else:
        slicing = False
        if 'slice' in kwargs:
            slicing = kwargs['slice']
            del kwargs['slice']

        if slicing:
            visualizeStencil3DBySlicing(stencil, **kwargs)
        else:
            visualizeStencil3D(stencil, **kwargs)


def visualizeStencil2D(stencil, axes=None, data=None):
    """
    Creates a matplotlib 2D plot of the stencil

    :param stencil: sequence of directions
    :param axes: optional matplotlib axes
    :param data: data to annotate the directions with, if none given, the indices are used
    """
    from matplotlib.patches import BoxStyle
    import matplotlib.pyplot as plt

    if axes is None:
        axes = plt.gca()

    text_box_style = BoxStyle("Round", pad=0.3)
    head_length = 0.1
    text_offset = 1.25

    if data is None:
        data = list(range(len(stencil)))

    for dir, annotation in zip(stencil, data):
        assert len(dir) == 2, "Works only for 2D stencils"

        if not(dir[0] == 0 and dir[1] == 0):
            axes.arrow(0, 0, dir[0], dir[1], head_width=0.08, head_length=head_length, color='k')

        axes.text(dir[0]*text_offset, dir[1]*text_offset, str(annotation), verticalalignment='center', zorder=30,
                  size='12', bbox=dict(boxstyle=text_box_style, facecolor='#00b6eb', alpha=0.85, linewidth=0))

    axes.set_axis_off()
    axes.set_aspect('equal')
    axes.set_xlim([-text_offset*1.1, text_offset*1.1])
    axes.set_ylim([-text_offset * 1.1, text_offset * 1.1])


def visualizeStencil3DBySlicing(stencil, sliceAxis=2, data=None):
    """
    Visualizes a 3D, first-neighborhood stencil by plotting 3 slices along a given axis

    :param stencil: stencil as sequence of directions
    :param sliceAxis: 0, 1, or 2 indicating the axis to slice through
    :param data: optional data to print as text besides the arrows
    :return:
    """
    import matplotlib.pyplot as plt

    for dir in stencil:
        for element in dir:
            assert element == -1 or element == 0 or element == 1, "This function can only first neighborhood stencils"

    f, axes = plt.subplots(1, 3)
    splittedDirections = [[], [], []]
    splittedData = [[], [], []]
    axesNames = ['x', 'y', 'z']

    for i, dir in enumerate(stencil):
        splitIdx = dir[sliceAxis] + 1
        reducedDir = tuple([element for j, element in enumerate(dir) if j != sliceAxis])
        splittedDirections[splitIdx].append(reducedDir)
        splittedData[splitIdx].append(i if data is None else data[i])

    for i in range(3):
        visualizeStencil2D(splittedDirections[i], axes[i], splittedData[i])
    for i in [-1, 0, 1]:
        axes[i+1].set_title("Cut at %s=%d" % (axesNames[sliceAxis], i))


def visualizeStencil3D(stencil, axes=None, data=None):
    """
    Draws 3D stencil into a 3D coordinate system, parameters are similar to :func:`visualizeStencil2D`
    If data is None, no labels are drawn. To draw the labels as in the 2D case, use ``data=list(range(len(stencil)))``
    """
    from matplotlib.patches import FancyArrowPatch
    from mpl_toolkits.mplot3d import proj3d
    import matplotlib.pyplot as plt
    from matplotlib.patches import BoxStyle
    from itertools import product, combinations
    import numpy as np

    class Arrow3D(FancyArrowPatch):
        def __init__(self, xs, ys, zs, *args, **kwargs):
            FancyArrowPatch.__init__(self, (0, 0), (0, 0), *args, **kwargs)
            self._verts3d = xs, ys, zs

        def draw(self, renderer):
            xs3d, ys3d, zs3d = self._verts3d
            xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
            self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
            FancyArrowPatch.draw(self, renderer)

    if axes is None:
        axes = plt.figure().gca(projection='3d')
        axes.set_aspect("equal")

    if data is None:
        data = [None] * len(stencil)

    text_offset = 1.25
    text_box_style = BoxStyle("Round", pad=0.3)

    # Draw cell (cube)
    r = [-1, 1]
    for s, e in combinations(np.array(list(product(r, r, r))), 2):
        if np.sum(np.abs(s - e)) == r[1] - r[0]:
            axes.plot3D(*zip(s, e), color="k", alpha=0.5)

    for dir, annotation in zip(stencil, data):
        assert len(dir) == 3, "Works only for 3D stencils"
        if not(dir[0] == 0 and dir[1] == 0 and dir[2] == 0):
            if dir[0] == 0:
                color = '#348abd'
            elif dir[1] == 0:
                color = '#fac364'
            elif sum([abs(d) for d in dir]) == 2:
                color = '#95bd50'
            else:
                color = '#808080'

            a = Arrow3D([0, dir[0]], [0, dir[1]], [0, dir[2]], mutation_scale=20, lw=2, arrowstyle="-|>", color=color)
            axes.add_artist(a)
        if annotation:
            axes.text(dir[0]*text_offset, dir[1]*text_offset, dir[2]*text_offset,
                      str(annotation), verticalalignment='center', zorder=30,
                      size='8', bbox=dict(boxstyle=text_box_style, facecolor='#777777', alpha=0.6, linewidth=0))

    axes.set_xlim([-text_offset*1.1, text_offset*1.1])
    axes.set_ylim([-text_offset * 1.1, text_offset * 1.1])
    axes.set_zlim([-text_offset * 1.1, text_offset * 1.1])
    axes.set_axis_off()