From 1b30965409fd5af8246ea5767591f34a21cdf93d Mon Sep 17 00:00:00 2001
From: Michael Kuron <mkuron@icp.uni-stuttgart.de>
Date: Fri, 10 Jul 2020 16:07:47 +0200
Subject: [PATCH] =?UTF-8?q?FVM:=20don=E2=80=99t=20scale=20the=20fluxes=20i?=
 =?UTF-8?q?n=20the=20continuity=20equation,=20better=20test?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pystencils/fd/finitevolumes.py | 18 +++----
 pystencils_tests/test_fvm.py   | 98 +++++++++++++++++++++-------------
 2 files changed, 68 insertions(+), 48 deletions(-)

diff --git a/pystencils/fd/finitevolumes.py b/pystencils/fd/finitevolumes.py
index 21054fc2..3dcec913 100644
--- a/pystencils/fd/finitevolumes.py
+++ b/pystencils/fd/finitevolumes.py
@@ -78,6 +78,9 @@ class FVM1stOrder:
         fluxes = [sp.Matrix(fluxes.tolist()[i]) if flux_field.index_dimensions > 1 else fluxes.tolist()[i] 
                   for i in range(self.dim)]
 
+        A0 = sum([sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm()
+                  for d in flux_field.staggered_stencil]) / self.dim
+
         discrete_fluxes = []
         for neighbor in flux_field.staggered_stencil:
             neighbor = ps.stencil.direction_string_to_offset(neighbor)
@@ -85,14 +88,14 @@ class FVM1stOrder:
             for i in range(1, self.dim):
                 directional_flux += fluxes[i] * int(neighbor[i])
             discrete_flux = discretize(directional_flux, neighbor)
-            discrete_fluxes.append(discrete_flux)
+            discrete_fluxes.append(discrete_flux / sp.Matrix(neighbor).norm())
 
         if flux_field.index_dimensions > 1:
-            return [ps.Assignment(lhs, rhs)
+            return [ps.Assignment(lhs, rhs / A0)
                     for i, d in enumerate(flux_field.staggered_stencil) if discrete_fluxes[i]
                     for lhs, rhs in zip(flux_field.staggered_vector_access(d), sp.simplify(discrete_fluxes[i]))]
         else:
-            return [ps.Assignment(flux_field.staggered_access(d), sp.simplify(discrete_fluxes[i]))
+            return [ps.Assignment(flux_field.staggered_access(d), sp.simplify(discrete_fluxes[i]) / A0)
                     for i, d in enumerate(flux_field.staggered_stencil)]
 
     def discrete_source(self):
@@ -184,14 +187,9 @@ class FVM1stOrder:
 
         neighbors = flux_field.staggered_stencil + [ps.stencil.inverse_direction_string(d)
                                                     for d in flux_field.staggered_stencil]
-        divergence = flux_field.staggered_vector_access(neighbors[0]) / \
-            sp.Matrix(ps.stencil.direction_string_to_offset(neighbors[0])).norm()
+        divergence = flux_field.staggered_vector_access(neighbors[0])
         for d in neighbors[1:]:
-            divergence += flux_field.staggered_vector_access(d) / \
-                sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm()
-        A0 = sum([sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm()
-                  for d in flux_field.staggered_stencil]) / self.dim
-        divergence /= A0
+            divergence += flux_field.staggered_vector_access(d)
 
         source = self.discrete_source()
         source = {s.lhs: s.rhs for s in source}
diff --git a/pystencils_tests/test_fvm.py b/pystencils_tests/test_fvm.py
index e0271ae6..719d2a37 100644
--- a/pystencils_tests/test_fvm.py
+++ b/pystencils_tests/test_fvm.py
@@ -3,28 +3,25 @@ import pystencils as ps
 import numpy as np
 import pytest
 from itertools import product
+from scipy.optimize import curve_fit
 
 
 @pytest.mark.parametrize("dim", [2, 3])
 def test_advection_diffusion(dim: int):
     # parameters
     if dim == 2:
-        domain_size = (32, 32)
-        flux_neighbors = 4
+        L = (32, 32)
     elif dim == 3:
-        domain_size = (16, 16, 16)
-        flux_neighbors = 13
+        L = (16, 16, 16)
 
-    dh = ps.create_data_handling(
-        domain_size=domain_size, periodicity=True, default_target='cpu')
+    dh = ps.create_data_handling(domain_size=L, periodicity=True, default_target='cpu')
 
     n_field = dh.add_array('n', values_per_cell=1)
-    j_field = dh.add_array('j', values_per_cell=flux_neighbors,
-                           field_type=ps.FieldType.STAGGERED_FLUX)
+    j_field = dh.add_array('j', values_per_cell=3 ** dim // 2, field_type=ps.FieldType.STAGGERED_FLUX)
     velocity_field = dh.add_array('v', values_per_cell=dim)
 
     D = 0.0666
-    time = 200
+    time = 100
 
     def grad(f):
         return sp.Matrix([ps.fd.diff(f, i) for i in range(dim)])
@@ -42,28 +39,27 @@ def test_advection_diffusion(dim: int):
 
     flux_kernel = ps.create_staggered_kernel(flux).compile()
 
-    pde_kernel = ps.create_kernel(
-        fvm_eq.discrete_continuity(j_field)).compile()
+    pde_kernel = ps.create_kernel(fvm_eq.discrete_continuity(j_field)).compile()
 
     sync_conc = dh.synchronization_function([n_field.name])
 
     # analytical density calculation
-    def density(pos: np.ndarray, time: int):
-        return (4 * np.pi * D * time)**(-1.5) * \
-            np.exp(-np.sum(np.square(pos), axis=dim) / (4 * D * time))
+    def density(pos: np.ndarray, time: int, D: float):
+        return (4 * np.pi * D * time)**(-dim / 2) * \
+            np.exp(-np.sum(np.square(pos), axis=-1) / (4 * D * time))
 
-    pos = np.zeros((*domain_size, dim))
-    xpos = np.arange(-domain_size[0] // 2, domain_size[0] // 2)
-    ypos = np.arange(-domain_size[1] // 2, domain_size[1] // 2)
+    pos = np.zeros((*L, dim))
+    xpos = np.arange(-L[0] // 2, L[0] // 2)
+    ypos = np.arange(-L[1] // 2, L[1] // 2)
 
     if dim == 2:
         pos[..., 1], pos[..., 0] = np.meshgrid(xpos, ypos)
     elif dim == 3:
-        zpos = np.arange(-domain_size[2] // 2, domain_size[2] // 2)
+        zpos = np.arange(-L[2] // 2, L[2] // 2)
         pos[..., 2], pos[..., 1], pos[..., 0] = np.meshgrid(xpos, ypos, zpos)
+    pos += 0.5
 
     def run(velocity: np.ndarray, time: int):
-        print(f"{velocity}, {time}")
         dh.fill(n_field.name, np.nan, ghost_layers=True, inner_ghost_layers=True)
         dh.fill(j_field.name, np.nan, ghost_layers=True, inner_ghost_layers=True)
 
@@ -71,8 +67,12 @@ def test_advection_diffusion(dim: int):
         for i in range(dim):
             dh.fill(velocity_field.name, velocity[i], i, ghost_layers=True, inner_ghost_layers=True)
         dh.fill(n_field.name, 0)
-        dh.fill(n_field.name, 1, slice_obj=ps.make_slice[[
-                dom // 2 for dom in domain_size]])
+        if dim == 2:
+            start = ps.make_slice[L[0] // 2 - 1:L[0] // 2 + 1, L[1] // 2 - 1:L[1] // 2 + 1]
+        else:
+            start = ps.make_slice[L[0] // 2 - 1:L[0] // 2 + 1, L[1] // 2 - 1:L[1] // 2 + 1,
+                                  L[2] // 2 - 1:L[2] // 2 + 1]
+        dh.fill(n_field.name, 2**-dim, slice_obj=start)
 
         sync_conc()
         for i in range(time):
@@ -80,12 +80,31 @@ def test_advection_diffusion(dim: int):
             dh.run_kernel(pde_kernel)
             sync_conc()
 
-        calc_density = density(pos - velocity * time, time)
-
-        np.testing.assert_allclose(dh.gather_array(
-            n_field.name), calc_density, atol=1e-2, rtol=0)
-
-    for vel in product(*[[0, -0.07, 0.05], [0, -0.03, 0.02], [0, -0.11, 0.13]][:dim]):
+        sim_density = dh.gather_array(n_field.name)
+        
+        # check that mass was conserved
+        assert np.isclose(sim_density.sum(), 1)
+        assert np.all(sim_density > 0)
+        
+        # check that the maximum is in the right place
+        peak = np.unravel_index(np.argmax(sim_density, axis=None), sim_density.shape)
+        assert np.allclose(peak, np.array(L) // 2 - 0.5 + velocity * time, atol=0.5)
+        
+        # check the concentration profile
+        if np.linalg.norm(velocity) == 0:
+            calc_density = density(pos - velocity * time, time, D)
+            target = [time, D]
+        
+            popt, _ = curve_fit(lambda x, t, D: density(x - velocity * time, t, D),
+                                pos.reshape(-1, dim),
+                                sim_density.reshape(-1),
+                                p0=target)
+        
+            assert np.isclose(popt[0], time, rtol=0.05)
+            assert np.isclose(popt[1], D, rtol=0.05)
+            assert np.allclose(calc_density, sim_density, atol=1e-4)
+
+    for vel in product(*[[0, -0.047, 0.041], [0, -0.031, 0.023], [0, -0.017, 0.011]][:dim]):
         run(np.array(vel), time)
 
 
@@ -189,7 +208,8 @@ def test_advection(dim):
     assert np.allclose(j1, j2)
 
 
-def test_ek():
+@pytest.mark.parametrize("stencil", ["D2Q5", "D2Q9"])
+def test_ek(stencil):
 
     # parameters
 
@@ -201,7 +221,7 @@ def test_ek():
 
     dh = ps.create_data_handling(L, periodicity=True, default_target='cpu')
     c = dh.add_array('c', values_per_cell=1)
-    j = dh.add_array('j', values_per_cell=dh.dim * 2, field_type=ps.FieldType.STAGGERED_FLUX)
+    j = dh.add_array('j', values_per_cell=int(stencil[-1]) // 2, field_type=ps.FieldType.STAGGERED_FLUX)
     Phi = dh.add_array('Φ', values_per_cell=1)
 
     # perform automatic discretization
@@ -219,20 +239,22 @@ def test_ek():
 
     x_staggered = - c[-1, 0] + c[0, 0] + z * (c[-1, 0] + c[0, 0]) / 2 * (Phi[-1, 0] - Phi[0, 0])
     y_staggered = - c[0, -1] + c[0, 0] + z * (c[0, -1] + c[0, 0]) / 2 * (Phi[0, -1] - Phi[0, 0])
-    xy_staggered = - c[-1, -1] + c[0, 0] + z * (c[-1, -1] + c[0, 0]) / 2 * (Phi[-1, -1] - Phi[0, 0])
-    xY_staggered = - c[-1, 1] + c[0, 0] + z * (c[-1, 1] + c[0, 0]) / 2 * (Phi[-1, 1] - Phi[0, 0])
+    xy_staggered = (- c[-1, -1] + c[0, 0]) / sp.sqrt(2) + \
+        z * (c[-1, -1] + c[0, 0]) / 2 * (Phi[-1, -1] - Phi[0, 0]) / sp.sqrt(2)
+    xY_staggered = (- c[-1, 1] + c[0, 0]) / sp.sqrt(2) + \
+        z * (c[-1, 1] + c[0, 0]) / 2 * (Phi[-1, 1] - Phi[0, 0]) / sp.sqrt(2)
+    A0 = (1 + sp.sqrt(2) if j.index_shape[0] == 4 else 1)
 
     jj = j.staggered_access
-    divergence = -1 / (1 + sp.sqrt(2) if j.index_shape[0] == 4 else 1) * \
-        sum([jj(d) / sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm() for d in j.staggered_stencil
-            + [ps.stencil.inverse_direction_string(d) for d in j.staggered_stencil]])
+    divergence = -1 * sum([jj(d) for d in j.staggered_stencil
+                          + [ps.stencil.inverse_direction_string(d) for d in j.staggered_stencil]])
 
     update = [ps.Assignment(c.center, c.center + divergence)]
-    flux = [ps.Assignment(j.staggered_access("W"), D * x_staggered),
-            ps.Assignment(j.staggered_access("S"), D * y_staggered)]
+    flux = [ps.Assignment(j.staggered_access("W"), D * x_staggered / A0),
+            ps.Assignment(j.staggered_access("S"), D * y_staggered / A0)]
     if j.index_shape[0] == 4:
-        flux += [ps.Assignment(j.staggered_access("SW"), D * xy_staggered),
-                 ps.Assignment(j.staggered_access("NW"), D * xY_staggered)]
+        flux += [ps.Assignment(j.staggered_access("SW"), D * xy_staggered / A0),
+                 ps.Assignment(j.staggered_access("NW"), D * xY_staggered / A0)]
 
     # compare
 
-- 
GitLab