Skip to content
Snippets Groups Projects
Commit 098e09aa authored by Michael Kuron's avatar Michael Kuron :mortar_board:
Browse files

Change tests from Guo to Luo if they don’t use SRT

parent d63fcb72
Branches
Tags
1 merge request!34Schiller force model to be used instead of Guo on non-SRT
Pipeline #24755 passed
%% Cell type:code id: tags:
``` python
from lbmpy.session import *
from lbmpy.relaxationrates import *
```
%% Cell type:markdown id: tags:
# Tutorial 05: Modifying a LBM method: Smagorinsky model
In this demo, we show how to modify a lattice Boltzmann method. As example we are going to add a simple turbulence model, by introducing a rule that locally computes the relaxation parameter dependent on the local strain rate tensor. The Smagorinsky model is implemented directly in *lbmpy* as well, however here we take the manual approach to demonstrate how a LB method can be changed in *lbmpy*.
## 1) Theoretical background
Since we have *sympy* available, we want to start out with the basic model equations and derive the concrete equations ourselves. This approach is less error prone, since the calculations are done by the computer algebra system, and oftentimes this approach is also more general and easier to understand.
### a) Smagorinsky model
The basic idea of the Smagorinsky turbulence model is to safe compute time, by not resolving the smallest eddies of the flow on the grid, but model them by an artifical dissipation term.
The energy dissipation of small scale vortices is taken into account by introducing a "turbulent viscosity". This additional viscosity depends on local flow properties, namely the local shear rates. The larger the local shear rates the higher the turbulent viscosity and the more artifical dissipation is added.
The total viscosity is
$$\nu_{total} = \nu_0 + \underbrace{(C_S \Delta)^2 |S|}_{\nu_{t}}$$
where $\nu_0$ is the normal viscosity, $C_S$ is the Smagorinsky constant, not to be confused with the speed of sound! Typical values of the Smagorinsky constant are between 0.1 - 0.2. The filter length $\Delta$ is chosen as 1 in lattice coordinates.
The quantity $|S|$ is computed from the local strain rate tensor $S$ that is given by
$$S_{ij} = \frac{1}{2} \left( \partial_i u_j + \partial_j u_i \right)$$
and
$$|S| = \sqrt{2 S_{ij} S_{ij}}$$
### b) LBM implementation of Smagorinsky model
To add the Smagorinsky model to a LB scheme one has to first compute the strain rate tensor $S_{ij}$ in each cell, and compute the turbulent viscosity $\nu_t$ from it. Then the local relaxation rate has to be adapted to match the total viscosity $\nu_{total}$ instead of the standard viscosity $\nu_0$.
A fortunate property of LB methods is, that the strain rate tensor can be computed locally from the non-equilibrium part of the distribution function. This is somewhat surprising, since the strain rate tensor contains first order derivatives. The strain rate tensor can be obtained by
$$S_{ij} = - \frac{3 \omega_s}{2 \rho_{(0)}} \Pi_{ij}^{(neq)}$$
where $\omega_s$ is the relaxation rate that determines the viscosity, $\rho_{(0)}$ is $\rho$ in compressible models and $1$ for incompressible schemes.
$\Pi_{ij}^{(neq)}$ is the second order moment tensor of the non-equilibrium part of the distribution functions $f^{(neq)} = f - f^{(eq)}$ and can be computed as
$$\Pi_{ij}^{(neq)} = \sum_q c_{qi} c_{qj} \; f_q^{(neq)}$$
%% Cell type:markdown id: tags:
We first have to find a closed form for $S_{ij}$ since in the formula above, it depends on $\omega$, which should be adapated according to $S_{ij}$.
So we compute $\omega$ and insert it into the formula for $S$:
%% Cell type:code id: tags:
``` python
τ_0, ρ, ω, ω_total, ω_0 = sp.symbols("tau_0 rho omega omega_total omega_0", positive=True, real=True)
ν_0, C_S, S, Π = sp.symbols("nu_0, C_S, |S|, Pi", positive=True, real=True)
Seq = sp.Eq(S, 3 * ω / 2 * Π)
Seq
```
%% Output
$$|S| = \frac{3 \Pi}{2} \omega$$
3⋅Π⋅ω
|S| = ─────
2
%% Cell type:markdown id: tags:
Note that we left of the minus, since we took the absolute value of both tensor. The absolute value is defined as above, with the factor of two inside the square root. The $\rho_{(0)}$ has been left out, remembering that $\Pi^{(neq)}$ has to be divided by $\rho$ in case of compressible models|.
Next, we compute $\omega$ from the total viscosity as given by the Smagorinsky equation:
%% Cell type:code id: tags:
``` python
relaxation_rate_from_lattice_viscosity(ν_0 + C_S ** 2 * S)
```
%% Output
$$\frac{2}{6 C_{S}^{2} |S| + 6 \nu_{0} + 1}$$
2
─────────────────────
2
6⋅C_S ⋅|S| + 6⋅ν₀ + 1
%% Cell type:markdown id: tags:
and insert it into the equation for $|S|$
%% Cell type:code id: tags:
``` python
Seq2 = Seq.subs(ω, relaxation_rate_from_lattice_viscosity(ν_0 + C_S **2 * S ))
Seq2
```
%% Output
$$|S| = \frac{3 \Pi}{6 C_{S}^{2} |S| + 6 \nu_{0} + 1}$$
3⋅Π
|S| = ─────────────────────
2
6⋅C_S ⋅|S| + 6⋅ν₀ + 1
%% Cell type:markdown id: tags:
This equation contains only known quantities, such that we can solve it for $|S|$.
Additionally we substitute the lattice viscosity $\nu_0$ by the original relaxation time $\tau_0$. The resulting equations get simpler using relaxation times instead of rates.
%% Cell type:code id: tags:
``` python
solveRes = sp.solve(Seq2, S)
assert len(solveRes) == 1
SVal = solveRes[0]
SVal = SVal.subs(ν_0, lattice_viscosity_from_relaxation_rate(1 / τ_0)).expand()
SVal
```
%% Output
$$- \frac{\tau_{0}}{6 C_{S}^{2}} + \frac{1}{12 C_{S}^{2}} \sqrt{72 C_{S}^{2} \Pi + 4 \tau_{0}^{2}}$$
___________________
╱ 2 2
τ₀ ╲╱ 72⋅C_S ⋅Π + 4⋅τ₀
- ────── + ──────────────────────
2 2
6⋅C_S 12⋅C_S
%% Cell type:markdown id: tags:
Knowning $|S|$ we can compute the total relaxation time using
$$\nu_{total} = \nu_0 +C_S^2 |S|$$
%% Cell type:code id: tags:
``` python
τ_val = 1 / (relaxation_rate_from_lattice_viscosity(lattice_viscosity_from_relaxation_rate(1/τ_0) + C_S**2 * SVal)).cancel()
τ_val
```
%% Output
$$\frac{\tau_{0}}{2} + \frac{1}{2} \sqrt{18 C_{S}^{2} \Pi + \tau_{0}^{2}}$$
_________________
╱ 2 2
τ₀ ╲╱ 18⋅C_S ⋅Π + τ₀
── + ────────────────────
2 2
%% Cell type:markdown id: tags:
To compute $\Pi^{(neq)}$ we use the following functions:
%% Cell type:code id: tags:
``` python
def second_order_moment_tensor(function_values, stencil):
assert len(function_values) == len(stencil)
dim = len(stencil[0])
return sp.Matrix(dim, dim, lambda i, j: sum(c[i] * c[j] * f for f, c in zip(function_values, stencil)))
def frobenius_norm(matrix, factor=1):
return sp.sqrt(sum(i*i for i in matrix) * factor)
```
%% Cell type:markdown id: tags:
In the next cell we construct equations that take an standard relaxation rate $\omega_0$ and compute a new relaxation rate $\omega_{total}$ according to the Smagorinksy model, using `τ_val` computed above
%% Cell type:code id: tags:
``` python
def smagorinsky_equations(ω_0, ω_total, method):
f_neq = sp.Matrix(method.pre_collision_pdf_symbols) - method.get_equilibrium_terms()
return [sp.Eq(τ_0, 1 / ω_0),
sp.Eq(Π, frobenius_norm(second_order_moment_tensor(f_neq, method.stencil), factor=2)),
sp.Eq(ω_total, 1 / τ_val)]
smagorinsky_equations(ω_0, ω_total, create_lb_method())
```
%% Output
$$\left [ \tau_{0} = \frac{1}{\omega_{0}}, \quad \Pi = \sqrt{4 \left(- f_{5} + f_{6} + f_{7} - f_{8} - u_{0} u_{1}\right)^{2} + 2 \left(f_{1} + f_{2} + f_{5} + f_{6} + f_{7} + f_{8} - \frac{\rho}{3} - u_{1}^{2}\right)^{2} + 2 \left(f_{3} + f_{4} + f_{5} + f_{6} + f_{7} + f_{8} - \frac{\rho}{3} - u_{0}^{2}\right)^{2}}, \quad \omega_{total} = \frac{1}{\frac{\tau_{0}}{2} + \frac{1}{2} \sqrt{18 C_{S}^{2} \Pi + \tau_{0}^{2}}}\right ]$$
⎡ ___________________________________________________________
⎢ ╱
⎢ 1 ╱ 2 ⎛
⎢τ₀ = ──, Π = ╱ 4⋅(-f₅ + f₆ + f₇ - f₈ - u₀⋅u₁) + 2⋅⎜f₁ + f₂ + f₅ + f₆ + f
⎢ ω₀ ╲╱ ⎝
________________________________________________________________
2 2
ρ 2⎞ ⎛ ρ 2⎞
₇ + f₈ - ─ - u₁ ⎟ + 2⋅⎜f₃ + f₄ + f₅ + f₆ + f₇ + f₈ - ─ - u₀ ⎟ , ωₜₒₜₐₗ = ───
3 ⎠ ⎝ 3 ⎠
τ₀
──
2
1 ⎥
──────────────────────⎥
_________________⎥
╱ 2 2 ⎥
╲╱ 18⋅C_S ⋅Π + τ₀ ⎥
+ ────────────────────⎥
2 ⎦
%% Cell type:markdown id: tags:
## 2) Application: Channel flow
Next we modify a *lbmpy* scenario to use the Smagorinsky model.
We create a MRT method, where we fix all relaxation rates except the relaxation rate that controls the viscosity.
%% Cell type:code id: tags:
``` python
method = create_lb_method(method='mrt', weighted=True, stencil='D2Q9', force=(1e-6, 0),
relaxation_rates=[0, 0, ω, 1.9, 1.9])
force_model='luo', relaxation_rates=[0, 0, ω, 1.9, 1.9])
method
```
%% Output
<lbmpy.methods.momentbased.MomentBasedLbMethod at 0x7f6445944b70>
%% Cell type:markdown id: tags:
Only the collision rule has to be changed. Thus we first construct the collision rule, add the Smagorinsky equations and create a normal scenario from the modified collision rule.
%% Cell type:code id: tags:
``` python
collision_rule = create_lb_collision_rule(lb_method=method)
collision_rule = collision_rule.new_with_substitutions({ω: ω_total})
collision_rule.subexpressions += smagorinsky_equations(ω, ω_total, method)
collision_rule.topological_sort(sort_subexpressions=True, sort_main_assignments=False)
collision_rule
```
%% Output
Assignment Collection for d_0,d_1,d_2,d_3,d_4,d_5,d_6,d_7,d_8
%% Cell type:markdown id: tags:
In the next cell the collision rule is simplified by extracting common subexpressions
%% Cell type:code id: tags:
``` python
from pystencils.simp import sympy_cse
#collision_rule = sympy_cse(collision_rule)
```
%% Cell type:markdown id: tags:
A channel scenario can be created from a modified collision rule:
%% Cell type:code id: tags:
``` python
ch = create_channel((300, 100), force=1e-6, collision_rule=collision_rule,
kernel_params={"C_S": 0.12, "omega": 1.999})
```
%% Cell type:code id: tags:
``` python
#show_code(ch.ast)
```
%% Cell type:code id: tags:
``` python
ch.run(5000)
```
%% Cell type:code id: tags:
``` python
plt.vector_field(ch.velocity[:, :])
np.max(ch.velocity[:, :])
```
%% Output
$$2.51715274224e-06$$
2.51715274224e-06
%% Cell type:markdown id: tags:
## Appendix: Strain rate tensor formula from Chapman Enskog
The connection between $S_{ij}$ and $\Pi_{ij}^{(neq)}$ can be seen using a Chapman Enskog expansion. Since *lbmpy* has a module that automatically does this expansions we can have a look at it:
%% Cell type:code id: tags:
``` python
from lbmpy.chapman_enskog import ChapmanEnskogAnalysis, CeMoment
from lbmpy.chapman_enskog.chapman_enskog import remove_higher_order_u
compressible_model = create_lb_method(stencil="D2Q9", compressible=True)
incompressible_model = create_lb_method(stencil="D2Q9", compressible=False)
ce_compressible = ChapmanEnskogAnalysis(compressible_model)
ce_incompressible = ChapmanEnskogAnalysis(incompressible_model)
```
%% Cell type:markdown id: tags:
The Chapman Enskog analysis yields expresssions for the moment
$\Pi = \Pi^{(eq)} + \epsilon \Pi^{(1)} + \epsilon^2 \Pi^{(2)} \cdots$
and the strain rate tensor is related to $\Pi^{(1)}$. However the best approximation we have for $\Pi^{(1)}$ is
$\Pi^{(neq)}$. For details, see the paper "Shear stress in lattice Boltzmann simulations" by Krüger, Varnik and Raabe from 2009.
Lets look at the values of $\Pi^{(1)}$ obtained from the Chapman enskog expansion:
%% Cell type:code id: tags:
``` python
Π_1_xy = CeMoment("\\Pi", moment_tuple=(1,1), superscript=1)
Π_1_xx = CeMoment("\\Pi", moment_tuple=(2,0), superscript=1)
Π_1_yy = CeMoment("\\Pi", moment_tuple=(0,2), superscript=1)
components = (Π_1_xx, Π_1_yy, Π_1_xy)
Π_1_xy_val = ce_compressible.higher_order_moments[Π_1_xy]
Π_1_xy_val
```
%% Output
$$\frac{1}{\omega_{0}} \left(\rho u_{0}^{2} {\partial^{(1)}_{0} u_{1}} + 2 \rho u_{0} u_{1} {\partial^{(1)}_{0} u_{0}} + 2 \rho u_{0} u_{1} {\partial^{(1)}_{1} u_{1}} + \rho u_{1}^{2} {\partial^{(1)}_{1} u_{0}} - \frac{\rho}{3} {\partial^{(1)}_{1} u_{0}} - \frac{\rho}{3} {\partial^{(1)}_{0} u_{1}} + u_{0}^{2} u_{1} {\partial^{(1)}_{0} \rho} + u_{0} u_{1}^{2} {\partial^{(1)}_{1} \rho}\right)$$
2 2 ρ⋅D(u_0)
ρ⋅u₀ ⋅D(u_1) + 2⋅ρ⋅u₀⋅u₁⋅D(u_0) + 2⋅ρ⋅u₀⋅u₁⋅D(u_1) + ρ⋅u₁ ⋅D(u_0) - ──────── -
3
──────────────────────────────────────────────────────────────────────────────
ω₀
ρ⋅D(u_1) 2 2
──────── + u₀ ⋅u₁⋅D(rho) + u₀⋅u₁ ⋅D(rho)
3
─────────────────────────────────────────
%% Cell type:markdown id: tags:
This term has lots of higher order error terms in it. We assume that $u$ is small in lattice coordinates, so if we neglect all terms in $u$ that are quadratic or higher we get:
%% Cell type:code id: tags:
``` python
remove_higher_order_u(Π_1_xy_val.expand())
```
%% Output
$$- \frac{\rho {\partial^{(1)}_{1} u_{0}}}{3 \omega_{0}} - \frac{\rho {\partial^{(1)}_{0} u_{1}}}{3 \omega_{0}}$$
ρ⋅D(u_0) ρ⋅D(u_1)
- ──────── - ────────
3⋅ω₀ 3⋅ω₀
%% Cell type:markdown id: tags:
Putting these steps together into a function, we can display them for the different cases quickly:
%% Cell type:code id: tags:
``` python
def get_Π_1(ce_analysis, component):
val = ce_analysis.higher_order_moments[component]
return remove_higher_order_u(val.expand())
```
%% Cell type:markdown id: tags:
Compressible case:
%% Cell type:code id: tags:
``` python
tuple(get_Π_1(ce_compressible, Pi) for Pi in components)
```
%% Output
$$\left ( - \frac{2 \rho {\partial^{(1)}_{0} u_{0}}}{3 \omega_{0}}, \quad - \frac{2 \rho {\partial^{(1)}_{1} u_{1}}}{3 \omega_{0}}, \quad - \frac{\rho {\partial^{(1)}_{1} u_{0}}}{3 \omega_{0}} - \frac{\rho {\partial^{(1)}_{0} u_{1}}}{3 \omega_{0}}\right )$$
⎛-2⋅ρ⋅D(u_0) -2⋅ρ⋅D(u_1) ρ⋅D(u_0) ρ⋅D(u_1)⎞
⎜────────────, ────────────, - ──────── - ────────⎟
⎝ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀ ⎠
%% Cell type:markdown id: tags:
Incompressible case:
%% Cell type:code id: tags:
``` python
tuple(get_Π_1(ce_incompressible, Pi) for Pi in components)
```
%% Output
$$\left ( \frac{2 u_{0} {\partial^{(1)}_{0} \rho}}{3 \omega_{0}} - \frac{2 {\partial^{(1)}_{0} u_{0}}}{3 \omega_{0}}, \quad \frac{2 u_{1} {\partial^{(1)}_{1} \rho}}{3 \omega_{0}} - \frac{2 {\partial^{(1)}_{1} u_{1}}}{3 \omega_{0}}, \quad \frac{u_{0} {\partial^{(1)}_{1} \rho}}{3 \omega_{0}} + \frac{u_{1} {\partial^{(1)}_{0} \rho}}{3 \omega_{0}} - \frac{{\partial^{(1)}_{1} u_{0}}}{3 \omega_{0}} - \frac{{\partial^{(1)}_{0} u_{1}}}{3 \omega_{0}}\right )$$
⎛2⋅u₀⋅D(rho) 2⋅D(u_0) 2⋅u₁⋅D(rho) 2⋅D(u_1) u₀⋅D(rho) u₁⋅D(rho) D(u_0
⎜─────────── - ────────, ─────────── - ────────, ───────── + ───────── - ─────
⎝ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀ 3⋅ω₀
) D(u_1)⎞
─ - ──────⎟
3⋅ω₀ ⎠
%% Cell type:markdown id: tags:
In the incompressible case has some terms $\partial \rho$ which are zero, since $\rho$ is assumed constant.
Leaving out the error terms we finally obtain:
$$\Pi_{ij}^{(neq)} \approx \Pi_{ij}^{(1)} = -\frac{2 \rho_{(0)}}{3 \omega_s} \left( \partial_i u_j + \partial_j u_i \right)$$
......
......@@ -41,6 +41,13 @@
publisher={APS}
}
@phdthesis{schiller2008thermal,
title={Thermal fluctuations and boundary conditions in the lattice Boltzmann method},
author={Schiller, Ulf Daniel},
year={2008},
school={Johannes Gutenberg Universit{\"a}t Mainz}
}
@article{Wohrwag2018,
archivePrefix = {arXiv},
......
......@@ -201,6 +201,7 @@ class Buick:
simple = Simple(self._force)
shear_relaxation_rate = get_shear_relaxation_rate(lb_method)
assert len(set(lb_method.relaxation_rates)) == 1, "Buick only works for SRT"
correction_factor = (1 - sp.Rational(1, 2) * shear_relaxation_rate)
return [correction_factor * t for t in simple(lb_method)]
......
......@@ -10,13 +10,13 @@ from lbmpy.stencils import get_stencil
def test_entropic_methods():
sc_kbc = create_lid_driven_cavity((20, 20), method='trt-kbc-n4', relaxation_rate=1.9999,
entropic_newton_iterations=3, entropic=True, compressible=True,
force=(-1e-10, 0))
force=(-1e-10, 0), force_model="luo")
sc_srt = create_lid_driven_cavity((40, 40), relaxation_rate=1.9999, lid_velocity=0.05, compressible=True,
force=(-1e-10, 0))
force=(-1e-10, 0), force_model="luo")
sc_entropic = create_lid_driven_cavity((40, 40), method='entropic-srt', relaxation_rate=1.9999,
lid_velocity=0.05, compressible=True, force=(-1e-10, 0))
lid_velocity=0.05, compressible=True, force=(-1e-10, 0), force_model="luo")
sc_srt.run(1000)
sc_kbc.run(1000)
......
......@@ -5,7 +5,7 @@ from lbmpy.scenarios import create_channel
def test_fluctuating_generation_pipeline():
ch = create_channel((10, 10), stencil='D2Q9', method='mrt', weighted=True, relaxation_rates=[1.5] * 5, force=1e-5,
fluctuating={'temperature': 1e-9}, kernel_params={'time_step': 1, 'seed': 312},
force_model='luo', fluctuating={'temperature': 1e-9}, kernel_params={'time_step': 1, 'seed': 312},
optimization={'cse_global': True})
ch.run(10)
......
from pystencils.session import *
from lbmpy.session import *
from lbmpy.macroscopic_value_kernels import macroscopic_values_setter
import pytest
import lbmpy.forcemodels
import pytest
from contextlib import ExitStack as does_not_raise
force_models = [fm.lower() for fm in dir(lbmpy.forcemodels) if fm[0].isupper()]
......@@ -25,14 +27,22 @@ def test_total_momentum(method, force_model, omega):
ρ = dh.add_array('rho')
u = dh.add_array('u', values_per_cell=dh.dim)
collision = create_lb_update_rule(method=method,
stencil=stencil,
relaxation_rate=omega,
compressible=True,
force_model=force_model,
force=F,
kernel_type='collide_only',
optimization={'symbolic_field': src})
expectation = does_not_raise()
skip = False
if force_model in ['guo', 'buick'] and method != 'srt':
expectation = pytest.raises(AssertionError)
skip = True
with expectation:
collision = create_lb_update_rule(method=method,
stencil=stencil,
relaxation_rate=omega,
compressible=True,
force_model=force_model,
force=F,
kernel_type='collide_only',
optimization={'symbolic_field': src})
if skip:
return
stream = create_stream_pull_with_output_kernel(collision.method, src, dst,
{'density': ρ, 'velocity': u})
......
......@@ -16,6 +16,7 @@ def test_split_number_of_operations():
common_params = {'stencil': stencil,
'method': method,
'compressible': compressible,
'force_model': 'luo',
'force': (1e-6, 1e-5, 1e-7)
}
ast_with_splitting = create_lb_ast(optimization={'split': True}, **common_params)
......
......@@ -4,7 +4,7 @@ known acceptable values.
"""
import sympy as sp
from lbmpy.forcemodels import Guo
from lbmpy.forcemodels import Luo
from lbmpy.methods import create_srt, create_trt, create_trt_with_magic_number
from lbmpy.methods.momentbasedsimplifications import cse_in_opposing_directions
from lbmpy.simplificationfactory import create_simplification_strategy
......@@ -55,13 +55,13 @@ def test_simplifications_trt_d2q9_compressible():
def test_simplifications_trt_d3q19_force_incompressible():
o1, o2 = sp.symbols("omega_1 omega_2")
force_model = Guo([sp.Rational(1, 3), sp.Rational(1, 2), sp.Rational(1, 5)])
force_model = Luo([sp.Rational(1, 3), sp.Rational(1, 2), sp.Rational(1, 5)])
method = create_trt(get_stencil("D3Q19"), o1, o2, compressible=False, force_model=force_model)
check_method(method, [268, 281, 0], [241, 175, 1])
def test_simplifications_trt_d3q19_force_compressible():
o1, o2 = sp.symbols("omega_1 omega_2")
force_model = Guo([sp.Rational(1, 3), sp.Rational(1, 2), sp.Rational(1, 5)])
force_model = Luo([sp.Rational(1, 3), sp.Rational(1, 2), sp.Rational(1, 5)])
method = create_trt_with_magic_number(get_stencil("D3Q19"), o1, compressible=False, force_model=force_model)
check_method(method, [270, 283, 1], [243, 177, 1])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment