diff --git a/pystencils/backends/cbackend.py b/pystencils/backends/cbackend.py index bb25a6a4ca9e119886d3b553ab937b188bad1173..94f3477290ecf79fcd8a095389a752d1505d67e2 100644 --- a/pystencils/backends/cbackend.py +++ b/pystencils/backends/cbackend.py @@ -6,7 +6,6 @@ import numpy as np import sympy as sp from sympy.core import S from sympy.logic.boolalg import BooleanFalse, BooleanTrue -from sympy.printing.ccode import C89CodePrinter from pystencils.astnodes import KernelFunction, Node from pystencils.cpu.vectorization import vec_all, vec_any @@ -330,10 +329,6 @@ class CustomSympyPrinter(CCodePrinter): def __init__(self): super(CustomSympyPrinter, self).__init__() self._float_type = create_type("float32") - if 'Min' in self.known_functions: - del self.known_functions['Min'] - if 'Max' in self.known_functions: - del self.known_functions['Max'] def _print_Pow(self, expr): """Don't use std::pow function, for small integer exponents, write as multiplication""" @@ -402,6 +397,8 @@ class CustomSympyPrinter(CCodePrinter): return f"({self._print(1 / sp.sqrt(expr.args[0]))})" elif isinstance(expr, sp.Abs): return f"abs({self._print(expr.args[0])})" + elif isinstance(expr, sp.Max): + return self._print(expr) elif isinstance(expr, sp.Mod): if expr.args[0].is_integer and expr.args[1].is_integer: return f"({self._print(expr.args[0])} % {self._print(expr.args[1])})" @@ -476,8 +473,25 @@ class CustomSympyPrinter(CCodePrinter): def _print_ConditionalFieldAccess(self, node): return self._print(sp.Piecewise((node.outofbounds_value, node.outofbounds_condition), (node.access, True))) - _print_Max = C89CodePrinter._print_Max - _print_Min = C89CodePrinter._print_Min + def _print_Max(self, expr): + def inner_print_max(args): + if len(args) == 1: + return self._print(args[0]) + half = len(args) // 2 + a = inner_print_max(args[:half]) + b = inner_print_max(args[half:]) + return f"(({a} > {b}) ? {a} : {b})" + return inner_print_max(expr.args) + + def _print_Min(self, expr): + def inner_print_min(args): + if len(args) == 1: + return self._print(args[0]) + half = len(args) // 2 + a = inner_print_min(args[:half]) + b = inner_print_min(args[half:]) + return f"(({a} < {b}) ? {a} : {b})" + return inner_print_min(expr.args) def _print_re(self, expr): return f"real({self._print(expr.args[0])})" @@ -575,6 +589,9 @@ class VectorizedCustomSympyPrinter(CustomSympyPrinter): result = self.instruction_set['&'].format(result, item) return result + def _print_Max(self, expr): + return "test" + def _print_Or(self, expr): result = self._scalarFallback('_print_Or', expr) if result: diff --git a/pystencils/fd/finitevolumes.py b/pystencils/fd/finitevolumes.py index c72ec8d90bf1f6afd1c0ec6df654e740911f577b..d2ddc3c79ebb9ad0bb846d71c19170272b69724d 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} @@ -204,7 +202,8 @@ def VOF(j: ps.field.Field, v: ps.field.Field, Ï: ps.field.Field): """Volume-of-fluid discretization of advection Args: - j: the staggeredfield to write the fluxes to + j: the staggered field to write the fluxes to. Should have a D2Q9/D3Q27 stencil. Other stencils work too, but + incur a small error (D2Q5/D3Q7: v^2, D3Q19: v^3). v: the flow velocity field Ï: the quantity to advect """ @@ -229,7 +228,8 @@ def VOF(j: ps.field.Field, v: ps.field.Field, Ï: ps.field.Field): overlap1 = [1 - sp.Abs(v1[i]) for i in range(len(v1))] overlap2 = [v1[i] for i in range(len(v1))] overlap = sp.Mul(*[(overlap1[i] if c[i] == 0 else overlap2[i]) for i in range(len(v1))]) - fluxes[d].append(Ï.neighbor_vector(c) * overlap * sp.Piecewise((1, cond), (0, True))) + sign = (c == 1).sum() % 2 * 2 - 1 + fluxes[d].append(sign * Ï.neighbor_vector(c) * overlap * sp.Piecewise((1, cond), (0, True))) for i, ff in enumerate(fluxes): fluxes[i] = ff[0] diff --git a/pystencils_tests/test_Min_Max.py b/pystencils_tests/test_Min_Max.py new file mode 100644 index 0000000000000000000000000000000000000000..18cd2d99d10ec0b03a38630d07679f3191281493 --- /dev/null +++ b/pystencils_tests/test_Min_Max.py @@ -0,0 +1,70 @@ +import sympy +import numpy +import pystencils +from pystencils.datahandling import create_data_handling + + +def test_max(): + dh = create_data_handling(domain_size=(10, 10), periodicity=True) + + x = dh.add_array('x', values_per_cell=1) + dh.fill("x", 0.0, ghost_layers=True) + y = dh.add_array('y', values_per_cell=1) + dh.fill("y", 1.0, ghost_layers=True) + z = dh.add_array('z', values_per_cell=1) + dh.fill("z", 2.0, ghost_layers=True) + + # test sp.Max with one argument + assignment_1 = pystencils.Assignment(x.center, sympy.Max(y.center + 3.3)) + ast_1 = pystencils.create_kernel(assignment_1) + kernel_1 = ast_1.compile() + + # test sp.Max with two arguments + assignment_2 = pystencils.Assignment(x.center, sympy.Max(0.5, y.center - 1.5)) + ast_2 = pystencils.create_kernel(assignment_2) + kernel_2 = ast_2.compile() + + # test sp.Max with many arguments + assignment_3 = pystencils.Assignment(x.center, sympy.Max(z.center, 4.5, y.center - 1.5, y.center + z.center)) + ast_3 = pystencils.create_kernel(assignment_3) + kernel_3 = ast_3.compile() + + dh.run_kernel(kernel_1) + assert numpy.all(dh.cpu_arrays["x"] == 4.3) + dh.run_kernel(kernel_2) + assert numpy.all(dh.cpu_arrays["x"] == 0.5) + dh.run_kernel(kernel_3) + assert numpy.all(dh.cpu_arrays["x"] == 4.5) + + +def test_min(): + dh = create_data_handling(domain_size=(10, 10), periodicity=True) + + x = dh.add_array('x', values_per_cell=1) + dh.fill("x", 0.0, ghost_layers=True) + y = dh.add_array('y', values_per_cell=1) + dh.fill("y", 1.0, ghost_layers=True) + z = dh.add_array('z', values_per_cell=1) + dh.fill("z", 2.0, ghost_layers=True) + + # test sp.Min with one argument + assignment_1 = pystencils.Assignment(x.center, sympy.Min(y.center + 3.3)) + ast_1 = pystencils.create_kernel(assignment_1) + kernel_1 = ast_1.compile() + + # test sp.Min with two arguments + assignment_2 = pystencils.Assignment(x.center, sympy.Min(0.5, y.center - 1.5)) + ast_2 = pystencils.create_kernel(assignment_2) + kernel_2 = ast_2.compile() + + # test sp.Min with many arguments + assignment_3 = pystencils.Assignment(x.center, sympy.Min(z.center, 4.5, y.center - 1.5, y.center + z.center)) + ast_3 = pystencils.create_kernel(assignment_3) + kernel_3 = ast_3.compile() + + dh.run_kernel(kernel_1) + assert numpy.all(dh.cpu_arrays["x"] == 4.3) + dh.run_kernel(kernel_2) + assert numpy.all(dh.cpu_arrays["x"] == - 0.5) + dh.run_kernel(kernel_3) + assert numpy.all(dh.cpu_arrays["x"] == - 0.5) diff --git a/pystencils_tests/test_fvm.py b/pystencils_tests/test_fvm.py index c973448e580eb300f729cf4b550a6ca11ab36c2b..c289ac54e7492cef31d74867847e2aefb2663d67 100644 --- a/pystencils_tests/test_fvm.py +++ b/pystencils_tests/test_fvm.py @@ -3,28 +3,24 @@ 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): +def 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 +38,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 +66,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,16 +79,152 @@ 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.08, 0.08], repeat=dim): - run(np.array(vel), time) + 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.1) + assert np.isclose(popt[1], D, rtol=0.1) + assert np.allclose(calc_density, sim_density, atol=1e-4) + + return lambda v: run(np.array(v), time) + + +advection_diffusion.runners = {} + + +@pytest.mark.parametrize("velocity", list(product([0, -0.047, 0.041], [0, -0.031, 0.023]))) +def test_advection_diffusion_2d(velocity): + if 2 not in advection_diffusion.runners: + advection_diffusion.runners[2] = advection_diffusion(2) + advection_diffusion.runners[2](velocity) + + +@pytest.mark.parametrize("velocity", list(product([0, -0.047, 0.041], [0, -0.031, 0.023], [0, -0.017, 0.011]))) +def test_advection_diffusion_3d(velocity): + if 3 not in advection_diffusion.runners: + advection_diffusion.runners[3] = advection_diffusion(3) + advection_diffusion.runners[3](velocity) + + +def VOF2(j: ps.field.Field, v: ps.field.Field, Ï: ps.field.Field, simplify=True): + """Volume-of-fluid discretization of advection + + Args: + j: the staggered field to write the fluxes to. Should have a D2Q9/D3Q27 stencil. Other stencils work too, but + incur a small error (D2Q5/D3Q7: v^2, D3Q19: v^3). + v: the flow velocity field + Ï: the quantity to advect + simplify: whether to simplify the generated expressions (slow, but makes them much more readable and faster) + """ + dim = j.spatial_dimensions + assert ps.FieldType.is_staggered(j) + + def assume_velocity(e): + if not simplify: + return e + repl = {} + for c in e.atoms(sp.StrictGreaterThan, sp.GreaterThan): + if isinstance(c.lhs, ps.field.Field.Access) and c.lhs.field == v and isinstance(c.rhs, sp.Number): + if c.rhs <= -1: + repl[c] = True + elif c.rhs >= 1: + repl[c] = False + for c in e.atoms(sp.StrictLessThan, sp.LessThan): + if isinstance(c.lhs, ps.field.Field.Access) and c.lhs.field == v and isinstance(c.rhs, sp.Number): + if c.rhs >= 1: + repl[c] = True + elif c.rhs <= -1: + repl[c] = False + for c in e.atoms(sp.Equality): + if isinstance(c.lhs, ps.field.Field.Access) and c.lhs.field == v and isinstance(c.rhs, sp.Number): + if c.rhs <= -1 or c.rhs >= 1: + repl[c] = False + return e.subs(repl) + + class AABB: + def __init__(self, corner0, corner1): + self.dim = len(corner0) + self.minCorner = sp.zeros(self.dim, 1) + self.maxCorner = sp.zeros(self.dim, 1) + for i in range(self.dim): + self.minCorner[i] = sp.Piecewise((corner0[i], corner0[i] < corner1[i]), (corner1[i], True)) + self.maxCorner[i] = sp.Piecewise((corner1[i], corner0[i] < corner1[i]), (corner0[i], True)) + + def intersect(self, other): + minCorner = [sp.Max(self.minCorner[d], other.minCorner[d]) for d in range(self.dim)] + maxCorner = [sp.Max(minCorner[d], sp.Min(self.maxCorner[d], other.maxCorner[d])) + for d in range(self.dim)] + return AABB(minCorner, maxCorner) + + @property + def volume(self): + v = sp.prod([self.maxCorner[d] - self.minCorner[d] for d in range(self.dim)]) + if simplify: + return sp.simplify(assume_velocity(v.rewrite(sp.Piecewise))) + else: + return v + + fluxes = [] + cell = AABB([-0.5] * dim, [0.5] * dim) + cell_s = AABB(sp.Matrix([-0.5] * dim) + v.center_vector, sp.Matrix([0.5] * dim) + v.center_vector) + for d, neighbor in enumerate(j.staggered_stencil): + c = sp.Matrix(ps.stencil.direction_string_to_offset(neighbor)[:dim]) + cell_n = AABB(sp.Matrix([-0.5] * dim) + c, sp.Matrix([0.5] * dim) + c) + cell_ns = AABB(sp.Matrix([-0.5] * dim) + c + v.neighbor_vector(neighbor), + sp.Matrix([0.5] * dim) + c + v.neighbor_vector(neighbor)) + fluxes.append(assume_velocity(Ï.center_vector * cell_s.intersect(cell_n).volume + - Ï.neighbor_vector(neighbor) * cell_ns.intersect(cell).volume)) + + assignments = [] + for i, d in enumerate(j.staggered_stencil): + for lhs, rhs in zip(j.staggered_vector_access(d).values(), fluxes[i].values()): + assignments.append(ps.Assignment(lhs, rhs)) + return assignments -def test_ek(): +@pytest.mark.parametrize("dim", [2, 3]) +def test_advection(dim): + L = (8,) * dim + 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=3 ** dh.dim // 2, field_type=ps.FieldType.STAGGERED_FLUX) + u = dh.add_array('u', values_per_cell=dh.dim) + + dh.cpu_arrays[c.name][:] = (np.random.random([l + 2 for l in L])) + dh.cpu_arrays[u.name][:] = (np.random.random([l + 2 for l in L] + [dim]) - 0.5) / 5 + + vof1 = ps.create_kernel(ps.fd.VOF(j, u, c)).compile() + dh.fill(j.name, np.nan, ghost_layers=True) + dh.run_kernel(vof1) + j1 = dh.gather_array(j.name).copy() + + vof2 = ps.create_kernel(VOF2(j, u, c, simplify=False)).compile() + dh.fill(j.name, np.nan, ghost_layers=True) + dh.run_kernel(vof2) + j2 = dh.gather_array(j.name) + + assert np.allclose(j1, j2) + + +@pytest.mark.parametrize("stencil", ["D2Q5", "D2Q9"]) +def test_ek(stencil): # parameters @@ -101,8 +236,7 @@ def test_ek(): dh = ps.create_data_handling(L, periodicity=True, default_target='cpu') c = dh.add_array('c', values_per_cell=1) - v = dh.add_array('v', values_per_cell=dh.dim) - 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 @@ -114,27 +248,28 @@ def test_ek(): disc = ps.fd.FVM1stOrder(c, flux_eq) flux_assignments = disc.discrete_flux(j) - advection_assignments = ps.fd.VOF(j, v, c) continuity_assignments = disc.discrete_continuity(j) # manual discretization 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