diff --git a/doc/notebooks/05_tutorial_modifying_method_smagorinsky.ipynb b/doc/notebooks/05_tutorial_modifying_method_smagorinsky.ipynb
index 20e1653b9692f9488ad55a36113cf52d0e70be9c..6c2ff255e9826e28b1b5baebca6190c480a38996 100644
--- a/doc/notebooks/05_tutorial_modifying_method_smagorinsky.ipynb
+++ b/doc/notebooks/05_tutorial_modifying_method_smagorinsky.ipynb
@@ -419,7 +419,7 @@
    ],
    "source": [
     "method = create_lb_method(method='mrt', weighted=True, stencil='D2Q9', force=(1e-6, 0),\n",
-    "                          relaxation_rates=[0, 0, ω, 1.9, 1.9])\n",
+    "                          force_model='luo', relaxation_rates=[0, 0, ω, 1.9, 1.9])\n",
     "method"
    ]
   },
diff --git a/doc/sphinx/lbmpy.bib b/doc/sphinx/lbmpy.bib
index 9620dc345ce71d66b4fca4719f64ea83f654732b..bceb010e1ee47c31a8109031f90491bcd5b818d0 100644
--- a/doc/sphinx/lbmpy.bib
+++ b/doc/sphinx/lbmpy.bib
@@ -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},
diff --git a/lbmpy/creationfunctions.py b/lbmpy/creationfunctions.py
index 4eca94b03d4db460eda7dfecd5d5e29555c909b8..4b3cc76760f9474031016d5feee9f172d585c415 100644
--- a/lbmpy/creationfunctions.py
+++ b/lbmpy/creationfunctions.py
@@ -41,8 +41,8 @@ General:
   compressible.
 - ``equilibrium_order=2``: order in velocity, at which the equilibrium moment/cumulant approximation is
   truncated. Order 2 is sufficient to approximate Navier-Stokes
-- ``force_model=None``: possible values: ``None``, ``'simple'``, ``'luo'``, ``'guo'`` ``'buick'``, or an instance of a
-  class implementing the same methods as the classes in :mod:`lbmpy.forcemodels`. For details, see
+- ``force_model=None``: possible values: ``None``, ``'simple'``, ``'luo'``, ``'guo'``, ``'buick'``, ``'schiller'``, or
+  an instance of a class implementing the same methods as the classes in :mod:`lbmpy.forcemodels`. For details, see
   :mod:`lbmpy.forcemodels`
 - ``force=(0,0,0)``: either constant force or a symbolic expression depending on field value
 - ``maxwellian_moments=True``: way to compute equilibrium moments/cumulants, if False the standard
@@ -387,7 +387,7 @@ def create_lb_method(**params):
 
     no_force_model = 'force_model' not in params or params['force_model'] == 'none' or params['force_model'] is None
     if not force_is_zero and no_force_model:
-        params['force_model'] = 'guo'
+        params['force_model'] = 'schiller'
 
     if 'force_model' in params:
         force_model = force_model_from_string(params['force_model'], params['force'][:dim])
@@ -479,6 +479,7 @@ def force_model_from_string(force_model_name, force_values):
         'buick': forcemodels.Buick,
         'silva': forcemodels.Buick,
         'edm': forcemodels.EDM,
+        'schiller': forcemodels.Schiller,
     }
     if force_model_name.lower() not in force_model_dict:
         raise ValueError("Unknown force model %s" % (force_model_name,))
diff --git a/lbmpy/forcemodels.py b/lbmpy/forcemodels.py
index 017927c37c8cb27f962497ab24770f35a11329de..ad57d27c9b95011386b11528cdf44d426985e155 100644
--- a/lbmpy/forcemodels.py
+++ b/lbmpy/forcemodels.py
@@ -7,7 +7,7 @@ Get started:
 ------------
 
 This module offers different models to introduce a body force in the lattice Boltzmann scheme.
-If you don't know which model to choose, use :class:`lbmpy.forcemodels.Guo`.
+If you don't know which model to choose, use :class:`lbmpy.forcemodels.Schiller`.
 For incompressible collision models the :class:`lbmpy.forcemodels.Buick` model can be better.
 
 
@@ -88,7 +88,7 @@ second moment nonzero   :class:`Luo`         :class:`Guo`
 
 import sympy as sp
 
-from lbmpy.relaxationrates import get_shear_relaxation_rate
+from lbmpy.relaxationrates import get_bulk_relaxation_rate, get_shear_relaxation_rate
 
 
 class Simple:
@@ -148,6 +148,7 @@ class Guo:
         luo = Luo(self._force)
 
         shear_relaxation_rate = get_shear_relaxation_rate(lb_method)
+        assert len(set(lb_method.relaxation_rates)) == 1, "Guo only works for SRT, use Schiller instead"
         correction_factor = (1 - sp.Rational(1, 2) * shear_relaxation_rate)
         return [correction_factor * t for t in luo(lb_method)]
 
@@ -158,6 +159,35 @@ class Guo:
         return default_velocity_shift(density, self._force)
 
 
+class Schiller:
+    r"""
+    Force model by Schiller  :cite:`schiller2008thermal`, equation 4.67
+    Equivalent to Guo but not restricted to SRT.
+    """
+    def __init__(self, force):
+        self._force = force
+
+    def __call__(self, lb_method):
+        u = sp.Matrix(lb_method.first_order_equilibrium_moment_symbols)
+        force = sp.Matrix(self._force)
+        
+        uf = u.dot(force) * sp.eye(len(force))
+        omega = get_shear_relaxation_rate(lb_method)
+        omega_bulk = get_bulk_relaxation_rate(lb_method)
+        G = (u * force.transpose() + force * u.transpose() - uf * sp.Rational(2, lb_method.dim)) * sp.Rational(1, 2) * \
+            (2 - omega) + uf * sp.Rational(1, lb_method.dim) * (2 - omega_bulk)
+
+        result = []
+        for direction, w_i in zip(lb_method.stencil, lb_method.weights):
+            direction = sp.Matrix(direction)
+            tr = sp.trace(G * (direction * direction.transpose() - sp.Rational(1, 3) * sp.eye(len(force))))
+            result.append(3 * w_i * (force.dot(direction) + sp.Rational(3, 2) * tr))
+        return result
+    
+    def macroscopic_velocity_shift(self, density):
+        return default_velocity_shift(density, self._force)
+
+
 class Buick:
     r"""
     This force model :cite:`buick2000gravity` has a force term with zero second moment. 
@@ -171,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)]
 
diff --git a/lbmpy/relaxationrates.py b/lbmpy/relaxationrates.py
index 70c9bdd5c99149546d55c2e01cc28321183c520b..adcdfcb65bdf6f1aac690fdc610935abafb0c8f1 100644
--- a/lbmpy/relaxationrates.py
+++ b/lbmpy/relaxationrates.py
@@ -1,6 +1,6 @@
 import sympy as sp
 
-from lbmpy.moments import is_shear_moment
+from lbmpy.moments import is_bulk_moment, is_shear_moment
 
 
 def relaxation_rate_from_lattice_viscosity(nu):
@@ -46,6 +46,18 @@ def get_shear_relaxation_rate(method):
                                       "Can not determine their relaxation rate automatically")
 
 
+def get_bulk_relaxation_rate(method):
+    """
+    The bulk moment is x^2 + y^2 + z^2, plus a constant for orthogonalization.
+    If this moment does not exist, the bulk relaxation is part of the shear relaxation.
+    The bulk relaxation rate determines the bulk viscosity in hydrodynamic LBM schemes.
+    """
+    for moment, relax_info in method.relaxation_info_dict.items():
+        if is_bulk_moment(moment, method.dim):
+            return relax_info.relaxation_rate
+    return get_shear_relaxation_rate(method)
+
+
 def relaxation_rate_scaling(omega, level_scale_factor):
     """Computes adapted omega for refinement.
 
diff --git a/lbmpy_tests/test_entropic.py b/lbmpy_tests/test_entropic.py
index 284570c1d4c874f8d11737a70346ade24ce94eba..47f6e841288e93f26037f5a38a29d2895f609de8 100644
--- a/lbmpy_tests/test_entropic.py
+++ b/lbmpy_tests/test_entropic.py
@@ -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)
diff --git a/lbmpy_tests/test_fluctuation.py b/lbmpy_tests/test_fluctuation.py
index 63078aa12ef5373ae5d350f17c6143894c9a5f33..ded0db9149789bc95af08829a7e68b171752252e 100644
--- a/lbmpy_tests/test_fluctuation.py
+++ b/lbmpy_tests/test_fluctuation.py
@@ -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)
diff --git a/lbmpy_tests/test_force.py b/lbmpy_tests/test_force.py
new file mode 100644
index 0000000000000000000000000000000000000000..79872b7b17d7927af3b3bfdbcd2724ebdcd6d2b7
--- /dev/null
+++ b/lbmpy_tests/test_force.py
@@ -0,0 +1,157 @@
+from pystencils.session import *
+from lbmpy.session import *
+from lbmpy.macroscopic_value_kernels import macroscopic_values_setter
+import lbmpy.forcemodels
+from lbmpy.moments import is_bulk_moment
+
+import pytest
+from contextlib import ExitStack as does_not_raise
+
+
+force_models = [fm.lower() for fm in dir(lbmpy.forcemodels) if fm[0].isupper()]
+
+def test_force_models_available():
+    assert 'guo' in force_models
+    assert 'luo' in force_models
+
+@pytest.mark.parametrize("method", ["srt", "trt"])
+@pytest.mark.parametrize("force_model", force_models)
+@pytest.mark.parametrize("omega", [0.5, 1.5])
+def test_total_momentum(method, force_model, omega):
+    L = (16, 16)
+    stencil = get_stencil("D2Q9")
+    F = [2e-4, -3e-4]
+
+    dh = ps.create_data_handling(L, periodicity=True, default_target='cpu')
+    src = dh.add_array('src', values_per_cell=len(stencil))
+    dst = dh.add_array_like('dst', 'src')
+    ρ = dh.add_array('rho')
+    u = dh.add_array('u', values_per_cell=dh.dim)
+
+    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})
+
+    opts = {'cpu_openmp': True, 
+            'cpu_vectorize_info': None,
+            'target': dh.default_target}
+
+    stream_kernel = ps.create_kernel(stream, **opts).compile()
+    collision_kernel = ps.create_kernel(collision, **opts).compile()
+
+    def init():
+        dh.fill(ρ.name, 1)
+        dh.fill(u.name, 0)
+
+        setter = macroscopic_values_setter(collision.method, velocity=(0,)*dh.dim, 
+                                       pdfs=src.center_vector, density=ρ.center)
+        kernel = ps.create_kernel(setter, ghost_layers=0).compile()
+        dh.run_kernel(kernel)
+
+    sync_pdfs = dh.synchronization_function([src.name])
+    def time_loop(steps):
+        dh.all_to_gpu()
+        for i in range(steps):
+            dh.run_kernel(collision_kernel)
+            sync_pdfs()
+            dh.run_kernel(stream_kernel)
+            dh.swap(src.name, dst.name)
+        dh.all_to_cpu()
+
+    t = 20
+    init()
+    time_loop(t)
+    total = np.sum(dh.gather_array(u.name), axis=(0,1))
+    assert np.allclose(total/np.prod(L)/F/t, 1)
+
+
+@pytest.mark.parametrize("stencil", ["D2Q9", "D3Q15", "D3Q19", "D3Q27"])
+@pytest.mark.parametrize("force_model", ["simple", "schiller"])
+def test_modes(stencil, force_model):
+    """check Schiller's force term in mode space"""
+    stencil = get_stencil(stencil)
+    dim = len(stencil[0])
+    
+    omega_s = sp.Symbol("omega_s")
+    omega_b = sp.Symbol("omega_b")
+    omega_o = sp.Symbol("omega_o")
+    omega_e = sp.Symbol("omega_e")
+    
+    F = [sp.Symbol(f"F_{i}") for i in range(dim)]
+    
+    method = create_lb_method(method="mrt", weighted=True,
+                              stencil=stencil,
+                              relaxation_rates=[omega_s, omega_b, omega_o, omega_e, omega_o, omega_e], 
+                              compressible=True,
+                              force_model=force_model,
+                              force=F)
+    
+    force_moments = sp.simplify(method.moment_matrix * sp.Matrix(method.force_model(method)))
+    
+    # The mass mode should be zero
+    assert force_moments[0] == 0
+    
+    # The momentum moments should contain the force
+    assert list(force_moments[1:dim+1]) == F
+    
+    if force_model == "schiller":
+        num_stresses = (dim*dim-dim)//2+dim
+        lambda_s, lambda_b = -omega_s, -omega_b
+
+        # The stress moments should match eq. 47 from https://doi.org/10.1023/A:1010414013942
+        u = method.first_order_equilibrium_moment_symbols
+        def traceless(m):
+            tr = sp.simplify(sp.Trace(m))
+            return m - tr/m.shape[0]*sp.eye(m.shape[0])
+        C = sp.Rational(1,2) * (2 + lambda_s) * (traceless(sp.Matrix(u) * sp.Matrix(F).transpose()) + \
+                                                traceless(sp.Matrix(F) * sp.Matrix(u).transpose())) + \
+            sp.Rational(1,method.dim) * (2 + lambda_b) * sp.Matrix(u).dot(F) * sp.eye(method.dim)
+
+        subs = {sp.Symbol(chr(ord("x")+i)) * sp.Symbol(chr(ord("x")+j)) : C[i,j]
+                for i in range(dim) for j in range(dim)}
+        for force_moment, moment in zip(force_moments[dim+1:dim+1+num_stresses],
+                                        method.moments[dim+1:dim+1+num_stresses]):
+            ref = moment.subs(subs)
+            diff = sp.simplify(ref - force_moment)
+            if is_bulk_moment(moment, dim):
+                assert diff == 0 or isinstance(diff, sp.Rational) # difference should be zero or a constant
+            else:
+                assert diff == 0 # difference should be zero
+
+        ff = sp.Matrix(method.force_model(method))
+        # Check eq. 4.53a from schiller2008thermal
+        assert sp.simplify(sum(ff)) == 0
+        # Check eq. 4.53b from schiller2008thermal
+        assert [sp.simplify(sum(ff[i] * stencil[i][j] for i in range(len(stencil)))) for j in range(dim)] == F
+        # Check eq. 4.61a from schiller2008thermal
+        ref = (2 + lambda_s)/2 * (traceless(sp.Matrix(u) * sp.Matrix(F).transpose()) + \
+                                 traceless(sp.Matrix(F) * sp.Matrix(u).transpose()))
+        s = sp.zeros(dim)
+        for i in range(0, len(stencil)):
+            s += ff[i] * traceless(sp.Matrix(stencil[i]) * sp.Matrix(stencil[i]).transpose())
+        assert sp.simplify(s-ref) == sp.zeros(dim)
+        # Check eq. 4.61b from schiller2008thermal
+        assert sp.simplify(sum(ff[i] * stencil[i][a]**2 for i in range(len(stencil)) for a in range(dim))
+                           - (2+lambda_b)*sp.Matrix(u).dot(F)) == 0
+
+        # All other moments should be zero
+        assert list(force_moments[dim+1+num_stresses:]) == [0] * (len(stencil)-(dim+1+num_stresses))
+    elif force_model == "simple":
+        # All other moments should be zero
+        assert list(force_moments[dim+1:]) == [0] * (len(stencil)-(dim+1))
diff --git a/lbmpy_tests/test_split_optimization.py b/lbmpy_tests/test_split_optimization.py
index 89e73133c487b37af3716a956911d7f46af2ae87..dd1093806086c5adc7875a72d69cc515bca4b282 100644
--- a/lbmpy_tests/test_split_optimization.py
+++ b/lbmpy_tests/test_split_optimization.py
@@ -17,6 +17,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)
diff --git a/lbmpy_tests/test_srt_trt_simplifications.py b/lbmpy_tests/test_srt_trt_simplifications.py
index ad0a96247593198ceb2e0db1dd536b5d9f8c2b83..ca830c4d3ce5f36a1fdc459be6bd598194c3bdb8 100644
--- a/lbmpy_tests/test_srt_trt_simplifications.py
+++ b/lbmpy_tests/test_srt_trt_simplifications.py
@@ -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, 284, 1], [243, 178, 1])