Lattice [Experimental]#

The Lattice class is an algebraic representation of the discrete lattice Boltzmann particle distribution fields, based on the pystencils.grids module. Lattice marks a paradigm shift in the design philosophy of lbmpy, as it relies on the efficient Esoteric Twist [GS17] streaming pattern, baked into the class.

Using Lattice#

This section introduces how to write lattice Boltzmann operators using the Lattice class. We will use a simple example of an advection-diffusion (ADE) LBM with a first-order equilibrium distribution (see [KrugerKK+17], chapter 8) for illustration.

Creating Lattices#

Creating a lattice, we define at least its name and its velocity set (stencil). Currently, Lattice supports only standard first-neighborhood stencils as modelled by StandardStencil. Optionally, a lattice can be associated with the cells or vertices of a patch, placing its discrete population vectors on either grid of the patch:

N = ps.symbols("N", dtype=ps.index_t)
Ω = ps.grids.Patch("Ω", (1, 1), num_cells=(N, N))

from lbmpy import lattice
from lbmpy.velocityspace import D2Q9

d2q9 = D2Q9()
f = lattice.Lattice("f", d2q9, Ω.cells)

You can furthermore modify the numerical data type and the buffer memory layout of Lattice; see the API documentation for details.

Accessing and Writing Populations#

Fundamentally, Lattice follows the same paradigms as TensorField. Created with a \(\mathrm{D}d\mathrm{Q}q\) stencil, it holds \(q\) populations on each site of a \(d\)-dimensional grid. Sites are accessed using offsets relative to the current site, and population values are accessed by their stencil indices (the order within the velocity set is defined by the stencil; see LBStencilBase):

  • f(i) refers to entry i of the particle distribution function (PDF) on the current site;

  • f[-1, 1](i) refers to entry i of the PDF on the south-eastern neighbor;

  • f[ox, oy](...) returns accesses to the entire PDF at the given lattice site as a tuple.

Example: The following example shows an operator that sets all distributions on the lattice f to the first-order equilibrium of the ADE LBM.

#   Define concentration and velocity fields
c = ps.grids.TensorField("c", Ω.cells, ())
u = ps.grids.TensorField("u", Ω.cells, (2,))

@ps.flow.operator
def set_ade_equilibrium(_eq):
    weights = d2q9.weights
    cs_sq = d2q9.speed_of_sound ** 2

    _eq.store[f(...)] = (
        weights[i] * c() * (1 + (d2q9[i][0] * u(0) + d2q9[i][1] * u(1)) / cs_sq)
        for i in range(9)
    )

Post-Collision Populations & Writing Collision Rules#

LBM schemes commonly decompose into two steps, collision and streaming:

(1)#\[ \boldsymbol{f}^{\ast} (\boldsymbol{x}) = \mathcal{C} \left( \boldsymbol{f} (\boldsymbol{x}) \right) \]
(2)#\[ f_i (\boldsymbol{x} + \boldsymbol{c_i}) = f_i^{\ast} (\boldsymbol{x}) \]

When writing an LB collision operator (1) on top of a lattice \(f\), the pre-collision populations are typically read from \(f\) as shown above. Post-collision values, however, must be written not to \(f\) directly, but via another accessor: f.post. This is necessary because Lattice realizes the streaming step via the Esoteric Twist scheme, which requires that post-collision populations are written to memory in a different storage pattern than pre-collision populations.

Caution

Writing to f.post overwrites the pre-collision populations in memory. For this reason, all pre-collision PDF values required by the collision operator should always be moved to symbols at the beginning of the operator.

Example: The following snippet shows how the collision operator of our advection-diffusion LBM can be written using the lattice \(f\).

@ps.flow.operator
def ade_collision(_eq):
    f_pre = ps.symbols("f_pre_:9")
    f_eq = ps.symbols("f_eq_:9")
    C, ω = ps.symbols("C, ω")

    weights = d2q9.weights
    cs_sq = d2q9.speed_of_sound ** 2

    #  Load pre-collision populations
    _eq.let[f_pre] = f(...)

    #  Compute concentration as zeroth moment
    _eq.let[C] = sum(f_pre)

    #  Set equilibrium populations
    _eq.let[f_eq] = (
        weights[i] * C * (1 + (d2q9[i][0] * u(0) + d2q9[i][1] * u(1)) / cs_sq)
        for i in range(9)
    )

    #  Perform relaxation and write post-collision values
    _eq.store[f.post(...)] = (
        ω * f_eq_i + (1 - ω) * f_i
        for f_eq_i, f_i in zip(f_eq, f_pre)
    )

    #   Optional: Write concentration `C` to field `c`
    _eq.store[c()] = C

The Streaming Step#

After writing to f.post in the collision operator, the streaming step (2) must be executed. This happens through the Advance operator, which switches the internal parity of the Esoteric Twist algorithms to shift post-collision populations into pre-collision PDFs on their destination sites. In an LBM integration loop, the Advance operation for a given lattice should always come immediately after that lattice’s collision operation.

Example: The following example shows an integration loop for the ADE LBM using the ade_collision operator and an instance of Advance, operating on data managed by a patch data container.

Ω_data = ps.grids.PatchData(Ω, {N: 32}, fields=[f, u, c])
advance_f = lattice.Advance(f)

def integrate_ade(nsteps):
    for _ in range(nsteps):
        ade_collision(Ω_data)
        advance_f(Ω_data)

Periodic Streaming#

(Partially) periodic domains occur regularly in LBM simulations. For Lattice, periodicity is handled as an extension to the streaming step. The operator lattice.PeriodicStream takes care of wrapping populations around the edges of a periodic domain. For each periodic lattice, PeriodicStream must be executed directly after Advance to effect periodic streaming.

Example: The following snippet modifies the integration loop from above to include fully periodic streaming.

periodic_stream = lattice.PeriodicStream(f, (True, True))

def integrate_ade(nsteps):
    for _ in range(nsteps):
        ade_collision(Ω_data)
        advance_f(Ω_data)
        periodic_stream(Ω_data)

API Reference#

Lattice

Discrete particle distribution field for the lattice Boltzmann method.

Collide

Apply an LBM collision operator to a Lattice.

SetEquilibrium

Initialize a Lattice to equilibrium state.

ExportMacroscopics

Export macroscopic observables from a lattice.

Advance

Execute the streaming step on a given lattice via Esoteric Twist timestep advancement.

PeriodicStream

Apply periodic boundary conditions as an extension to the streaming step.

GridAlignedHBB

Zero-velocity halfway bounce-back boundary condition.