diff --git a/pystencils/fd/finitevolumes.py b/pystencils/fd/finitevolumes.py index 588a4a45b2bab9b5aeb652d7c632902f667ddc8c..64f9db6900dbd884c7fef5b05b6d9d6e7243e657 100644 --- a/pystencils/fd/finitevolumes.py +++ b/pystencils/fd/finitevolumes.py @@ -15,7 +15,7 @@ class FVM1stOrder: flux: a list of sympy expressions that specify the flux, one for each cartesian direction source: a list of sympy expressions that specify the source """ - def __init__(self, field: ps.field.Field, flux=0, source=0): + def __init__(self, field: ps.field.Field, flux=0, source=0, flux_scaling: bool = False): def normalize(f, shape): shape = tuple(s for s in shape if s != 1) if not shape: @@ -30,6 +30,7 @@ class FVM1stOrder: self.dim = self.c.spatial_dimensions self.j = normalize(flux, (self.dim, ) + self.c.index_shape) self.q = normalize(source, self.c.index_shape) + self.flux_scaling = flux_scaling def discrete_flux(self, flux_field: ps.field.Field): """Return a list of assignments for the discrete fluxes @@ -85,14 +86,22 @@ class FVM1stOrder: for i in range(1, self.dim): directional_flux += fluxes[i] * int(neighbor[i]) discrete_flux = discretize(directional_flux, neighbor) + if self.flux_scaling: + discrete_flux /= sp.Matrix(neighbor).norm() discrete_fluxes.append(discrete_flux) + + if self.flux_scaling: + A0 = sum([sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm() + for d in flux_field.staggered_stencil]) / self.dim + else: + A0 = 1 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): @@ -185,13 +194,14 @@ 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() + (sp.Matrix(ps.stencil.direction_string_to_offset(neighbors[0])).norm() if not self.flux_scaling else 1) 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 + (sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm() if not self.flux_scaling else 1) + if not self.flux_scaling: + A0 = sum([sp.Matrix(ps.stencil.direction_string_to_offset(d)).norm() + for d in flux_field.staggered_stencil]) / self.dim + divergence /= A0 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 09b3e0be3dfc919e6c73c021009b9e87b65471ec..b33049a52b6775bdbb20b49992561ebc0406d38b 100644 --- a/pystencils_tests/test_fvm.py +++ b/pystencils_tests/test_fvm.py @@ -12,7 +12,7 @@ def test_advection_diffusion(dim: int): domain_size = (32, 32) flux_neighbors = 4 elif dim == 3: - domain_size = (16, 16, 16) + domain_size = (32, 32, 32) flux_neighbors = 13 domain_center = [dom // 2 for dom in domain_size] @@ -26,14 +26,13 @@ def test_advection_diffusion(dim: int): velocity_field = dh.add_array('v', values_per_cell=dim) D = 0.0666 - time = 200 - time_advection = 50 + time = 300 def grad(f): return sp.Matrix([ps.fd.diff(f, i) for i in range(dim)]) flux_eq = - D * grad(n_field) - fvm_eq = ps.fd.FVM1stOrder(n_field, flux=flux_eq) + fvm_eq = ps.fd.FVM1stOrder(n_field, flux=flux_eq, flux_scaling=True) vof_adv = ps.fd.VOF(j_field, velocity_field, n_field) @@ -44,27 +43,27 @@ def test_advection_diffusion(dim: int): flux.append(ps.Assignment(adv.lhs, adv.rhs + div.rhs)) flux_kernel = ps.create_staggered_kernel(flux).compile() - adv_kernel = ps.create_staggered_kernel(vof_adv).compile() - + pde_kernel = ps.create_kernel( fvm_eq.discrete_continuity(j_field)).compile() sync_conc = dh.synchronization_function([n_field.name]) - # analytical density calculation + # analytical density calculation with first eight neighbors in square domain 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)) + return sum((4 * np.pi * D * time) ** (-dim / 2) * np.exp( + -np.sum(np.square(pos + np.asarray(shift)), axis=dim) / (4 * D * time)) + for shift in product([0, dh.shape[0], -dh.shape[0]], repeat=dim)) 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) if dim == 2: - pos[..., 1], pos[..., 0] = np.meshgrid(xpos, ypos) + pos[..., 0], pos[..., 1] = np.meshgrid(xpos, ypos, indexing='ij') elif dim == 3: zpos = np.arange(-domain_size[2] // 2, domain_size[2] // 2) - pos[..., 2], pos[..., 1], pos[..., 0] = np.meshgrid(xpos, ypos, zpos) + pos[..., 0], pos[..., 1], pos[..., 2] = np.meshgrid(xpos, ypos, zpos, indexing='ij') def run(velocity: np.ndarray, time: int): print(f"{velocity}, {time}") @@ -82,50 +81,24 @@ def test_advection_diffusion(dim: int): dh.run_kernel(flux_kernel) 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) - assert dh.min(n_field.name) >= 0 - for vel in product([0, -0.08, 0.08], repeat=dim): - run(np.array(vel), time) + simulation_density = dh.gather_array(n_field.name) + analytic_density = density(pos - velocity * time, time) + peak_position = tuple(int(val) for val in domain_center + velocity * time) - def run_advection_only(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) - - # set initial values for velocity and density - 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[domain_center]) + analytical_maximum = analytic_density[peak_position] + relative_maximum_error = np.abs(simulation_density[peak_position] - analytical_maximum) / analytical_maximum - sync_conc() - for i in range(time): - dh.run_kernel(adv_kernel) - dh.run_kernel(pde_kernel) - sync_conc() - - max_direction = np.unravel_index(np.argmax(dh.gather_array(n_field.name)), domain_size) - np.asarray(domain_center) - norm = max(abs(max_direction)) - max_direction = max_direction / (norm if norm > 0 else 1) - - velocity_direction = velocity - norm = max(abs(velocity_direction)) - velocity_direction = velocity_direction / (norm if norm > 0 else 1) - - np.testing.assert_array_equal(max_direction, velocity_direction) assert dh.min(n_field.name) >= 0 + assert relative_maximum_error <= 0.18 + np.testing.assert_array_equal(np.unravel_index(np.argmax(simulation_density), dh.shape), peak_position) + np.testing.assert_allclose(simulation_density, analytic_density, atol=5e-4) - for vel in product([0, -0.6, 0.6], repeat=dim): - run_advection_only(np.array(vel), time_advection) + for vel in product([0, -0.02, 0.02], repeat=dim): + run(np.array(vel), time) def test_ek(): - # parameters L = (40, 40) @@ -147,7 +120,7 @@ def test_ek(): flux_eq = -D * Gradient(c) + D * z * c.center * Gradient(Phi) - disc = ps.fd.FVM1stOrder(c, flux_eq) + disc = ps.fd.FVM1stOrder(c, flux_eq, flux_scaling=False) flux_assignments = disc.discrete_flux(j) advection_assignments = ps.fd.VOF(j, v, c) continuity_assignments = disc.discrete_continuity(j)