From 32faae1148eca9b7404d9b8a41677e85ef2866c0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 11 Jul 2019 13:17:55 -0500 Subject: [PATCH 001/209] Experimental Stokes wrapper with Biharmonic Kernel --- pytential/symbolic/stokes.py | 187 ++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 82 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 7e47bdb22..ff25e94a6 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -24,7 +24,65 @@ import numpy as np from pytential import sym -from sumpy.kernel import StokesletKernel, StressletKernel, LaplaceKernel +from sumpy.symbolic import pymbolic_real_norm_2 +from pymbolic.primitives import make_sym_vector +from pymbolic import var +from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, + ExpressionKernel, AxisTargetDerivative, KernelArgument) +import loopy as lp + + +class StokesletKernelExperimental(ExpressionKernel): + init_arg_names = ("dim", "viscosity_mu_name") + + def __init__(self, dim, viscosity_mu_name="mu"): + r""" + :arg viscosity_mu_name: The argument name to use for + dynamic viscosity :math:`\mu` the then generating functions to + evaluate this kernel. + """ + mu = var(viscosity_mu_name) + d = make_sym_vector("d", dim) + r = pymbolic_real_norm_2(d) + + if dim == 2: + expr = r**2*var("log")(r)/2 - 3*r**2/4 + scaling = -1/(4*var("pi")*mu) + elif dim == 3: + expr = -r + scaling = -1/(8*var("pi")*mu) + elif dim is None: + expr = None + scaling = None + else: + raise RuntimeError("unsupported dimensionality") + + self.viscosity_mu_name = viscosity_mu_name + + super(StokesletKernelExperimental, self).__init__( + dim, + expression=expr, + global_scaling_const=scaling, + is_complex_valued=False) + + def __getinitargs__(self): + return (self.dim, self.viscosity_mu_name) + + def update_persistent_hash(self, key_hash, key_builder): + key_hash.update(type(self).__name__.encode()) + key_builder.rec(key_hash, + (self.dim, self.viscosity_mu_name)) + + def __repr__(self): + return "StokesletKnl%dD" % (self.dim) + + def get_args(self): + return [ + KernelArgument( + loopy_arg=lp.ValueArg(self.viscosity_mu_name, np.float64), + )] + + mapper_method = "map_stokeslet_kernel" class StokesletWrapper(object): @@ -56,28 +114,38 @@ class StokesletWrapper(object): for the same kernel in a different ordering. """ - def __init__(self, dim=None): - + def __init__(self, dim=None, experimental=True): + self.experimental = experimental self.dim = dim - if dim == 2: - self.kernel_dict = { - (2, 0): StokesletKernel(dim=2, icomp=0, jcomp=0), - (1, 1): StokesletKernel(dim=2, icomp=0, jcomp=1), - (0, 2): StokesletKernel(dim=2, icomp=1, jcomp=1) - } + if not (dim == 3 or dim == 2): + raise ValueError("unsupported dimension given to StokesletWrapper") - elif dim == 3: - self.kernel_dict = { - (2, 0, 0): StokesletKernel(dim=3, icomp=0, jcomp=0), - (1, 1, 0): StokesletKernel(dim=3, icomp=0, jcomp=1), - (1, 0, 1): StokesletKernel(dim=3, icomp=0, jcomp=2), - (0, 2, 0): StokesletKernel(dim=3, icomp=1, jcomp=1), - (0, 1, 1): StokesletKernel(dim=3, icomp=1, jcomp=2), - (0, 0, 2): StokesletKernel(dim=3, icomp=2, jcomp=2) - } + self.kernel_dict = {} + if self.experimental: + self._kernel = StokesletKernelExperimental(dim=dim) + for i in range(dim): + kernel_d1 = AxisTargetDerivative(i, self._kernel) + for j in range(i, dim): + self.kernel_dict[(i, j)] = AxisTargetDerivative(j, kernel_d1) else: - raise ValueError("unsupported dimension given to StokesletWrapper") + for i in range(dim): + for j in range(i, dim): + self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, + jcomp=j) + + for i in range(dim): + for j in range(i): + self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] + + def map_func_to_expr(self, idx, func): + if not self.experimental: + return func(self.kernel_dict[idx]) + + if idx[0] != idx[1]: + return func(self.kernel_dict[idx]) + + return -sum(func(self.kernel_dict[(i, i)]) for i in range(self.dim) if i != idx[0]) def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel @@ -94,29 +162,14 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for the average of the two one-sided boundary limits. """ - sym_expr = np.empty((self.dim,), dtype=object) + sym_expr = np.zeros((self.dim,), dtype=object) for comp in range(self.dim): - - # Start variable count for kernel with 1 for the requested result - # component - base_count = np.zeros(self.dim, dtype=np.int) - base_count[comp] += 1 - for i in range(self.dim): - var_ctr = base_count.copy() - var_ctr[i] += 1 - ctr_key = tuple(var_ctr) - - if i < 1: - sym_expr[comp] = sym.IntG( - self.kernel_dict[ctr_key], density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - - else: - sym_expr[comp] = sym_expr[comp] + sym.IntG( - self.kernel_dict[ctr_key], density_vec_sym[i], + func = lambda knl: sym.IntG( + knl, density_vec_sym[i], qbx_forced_limit=qbx_forced_limit, mu=mu_sym) + sym_expr[comp] += self.map_func_to_expr((comp, i), func) return sym_expr @@ -125,17 +178,12 @@ def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): from pytential.symbolic.mappers import DerivativeTaker kernel = LaplaceKernel(dim=self.dim) + sym_expr = 0 for i in range(self.dim): - - if i < 1: - sym_expr = DerivativeTaker(i).map_int_g( - sym.S(kernel, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit)) - else: - sym_expr = sym_expr + (DerivativeTaker(i).map_int_g( - sym.S(kernel, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit))) + sym_expr += (DerivativeTaker(i).map_int_g( + sym.S(kernel, density_vec_sym[i], + qbx_forced_limit=qbx_forced_limit))) return sym_expr @@ -159,34 +207,16 @@ def apply_derivative(self, deriv_dir, density_vec_sym, from pytential.symbolic.mappers import DerivativeTaker - sym_expr = np.empty((self.dim,), dtype=object) + sym_expr = np.zeros((self.dim,), dtype=object) for comp in range(self.dim): - - # Start variable count for kernel with 1 for the requested result - # component - base_count = np.zeros(self.dim, dtype=np.int) - base_count[comp] += 1 - for i in range(self.dim): - var_ctr = base_count.copy() - var_ctr[i] += 1 - ctr_key = tuple(var_ctr) - - if i < 1: - sym_expr[comp] = DerivativeTaker(deriv_dir).map_int_g( - sym.IntG(self.kernel_dict[ctr_key], - density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym)) - - else: - sym_expr[comp] = sym_expr[comp] + DerivativeTaker( - deriv_dir).map_int_g( - sym.IntG(self.kernel_dict[ctr_key], + func = lambda knl: DerivativeTaker(deriv_dir).map_int_g( + sym.IntG(knl, density_vec_sym[i], qbx_forced_limit=qbx_forced_limit, mu=mu_sym)) + sym_expr[comp] += self.map_func_to_expr((comp, i), func) return sym_expr @@ -221,7 +251,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, import itertools - sym_expr = np.empty((self.dim,), dtype=object) + sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = StressletWrapper(dim=self.dim) for comp in range(self.dim): @@ -237,18 +267,11 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, var_ctr[j] += 1 ctr_key = tuple(var_ctr) - if i + j < 1: - sym_expr[comp] = dir_vec_sym[i] * sym.IntG( - stresslet_obj.kernel_dict[ctr_key], - density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - - else: - sym_expr[comp] = sym_expr[comp] + dir_vec_sym[i] * sym.IntG( - stresslet_obj.kernel_dict[ctr_key], - density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym) + sym_expr[comp] += dir_vec_sym[i] * sym.IntG( + stresslet_obj.kernel_dict[ctr_key], + density_vec_sym[j], + qbx_forced_limit=qbx_forced_limit, + mu=mu_sym) return sym_expr From f418fe78cd8543a4511bc582e7aaf09c17efe9f8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 12 Jul 2019 08:27:58 -0500 Subject: [PATCH 002/209] Fix formatting --- pytential/symbolic/stokes.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index ff25e94a6..a574b5d8a 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -145,7 +145,8 @@ def map_func_to_expr(self, idx, func): if idx[0] != idx[1]: return func(self.kernel_dict[idx]) - return -sum(func(self.kernel_dict[(i, i)]) for i in range(self.dim) if i != idx[0]) + return -sum(func(self.kernel_dict[(i, i)]) for i + in range(self.dim) if i != idx[0]) def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel @@ -166,9 +167,9 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for comp in range(self.dim): for i in range(self.dim): - func = lambda knl: sym.IntG( - knl, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) + def func(knl): + return sym.IntG(knl, density_vec_sym[i], + qbx_forced_limit=qbx_forced_limit, mu=mu_sym) sym_expr[comp] += self.map_func_to_expr((comp, i), func) return sym_expr @@ -211,11 +212,12 @@ def apply_derivative(self, deriv_dir, density_vec_sym, for comp in range(self.dim): for i in range(self.dim): - func = lambda knl: DerivativeTaker(deriv_dir).map_int_g( - sym.IntG(knl, - density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym)) + def func(knl): + return DerivativeTaker(deriv_dir).map_int_g( + sym.IntG(knl, + density_vec_sym[i], + qbx_forced_limit=qbx_forced_limit, + mu=mu_sym)) sym_expr[comp] += self.map_func_to_expr((comp, i), func) return sym_expr From 498c1cb5a064bdd6b3d5b29ded0260067524a222 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 9 Sep 2020 03:29:02 -0500 Subject: [PATCH 003/209] Use BiharmonicKernel directly --- pytential/symbolic/stokes.py | 110 +++++++++++------------------------ 1 file changed, 34 insertions(+), 76 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index a574b5d8a..a5282729a 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -24,65 +24,8 @@ import numpy as np from pytential import sym -from sumpy.symbolic import pymbolic_real_norm_2 -from pymbolic.primitives import make_sym_vector -from pymbolic import var from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, - ExpressionKernel, AxisTargetDerivative, KernelArgument) -import loopy as lp - - -class StokesletKernelExperimental(ExpressionKernel): - init_arg_names = ("dim", "viscosity_mu_name") - - def __init__(self, dim, viscosity_mu_name="mu"): - r""" - :arg viscosity_mu_name: The argument name to use for - dynamic viscosity :math:`\mu` the then generating functions to - evaluate this kernel. - """ - mu = var(viscosity_mu_name) - d = make_sym_vector("d", dim) - r = pymbolic_real_norm_2(d) - - if dim == 2: - expr = r**2*var("log")(r)/2 - 3*r**2/4 - scaling = -1/(4*var("pi")*mu) - elif dim == 3: - expr = -r - scaling = -1/(8*var("pi")*mu) - elif dim is None: - expr = None - scaling = None - else: - raise RuntimeError("unsupported dimensionality") - - self.viscosity_mu_name = viscosity_mu_name - - super(StokesletKernelExperimental, self).__init__( - dim, - expression=expr, - global_scaling_const=scaling, - is_complex_valued=False) - - def __getinitargs__(self): - return (self.dim, self.viscosity_mu_name) - - def update_persistent_hash(self, key_hash, key_builder): - key_hash.update(type(self).__name__.encode()) - key_builder.rec(key_hash, - (self.dim, self.viscosity_mu_name)) - - def __repr__(self): - return "StokesletKnl%dD" % (self.dim) - - def get_args(self): - return [ - KernelArgument( - loopy_arg=lp.ValueArg(self.viscosity_mu_name, np.float64), - )] - - mapper_method = "map_stokeslet_kernel" + AxisTargetDerivative, BiharmonicKernel) class StokesletWrapper(object): @@ -114,20 +57,26 @@ class StokesletWrapper(object): for the same kernel in a different ordering. """ - def __init__(self, dim=None, experimental=True): - self.experimental = experimental + def __init__(self, dim=None, use_biharmonic=True): + self.use_biharmonic = use_biharmonic + self.const = 0 self.dim = dim if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") self.kernel_dict = {} - if self.experimental: - self._kernel = StokesletKernelExperimental(dim=dim) + if self.use_biharmonic: + # Note that for 3D, the following holds, + # $K_{i, j} = \nabla \nabla + \mathrm{I} \nabla^2 r $ + # where $r$ is the fundamental solution to + _kernel = BiharmonicKernel(dim=dim) for i in range(dim): - kernel_d1 = AxisTargetDerivative(i, self._kernel) + kernel_d1 = AxisTargetDerivative(i, _kernel) for j in range(i, dim): self.kernel_dict[(i, j)] = AxisTargetDerivative(j, kernel_d1) + if dim == 2: + self.const = 3/2 else: for i in range(dim): for j in range(i, dim): @@ -138,15 +87,24 @@ def __init__(self, dim=None, experimental=True): for j in range(i): self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] - def map_func_to_expr(self, idx, func): - if not self.experimental: - return func(self.kernel_dict[idx]) + def map_func_to_expr(self, idx, func, **kwargs): + if not self.use_biharmonic: + return func(self.kernel_dict[idx], **kwargs) + + mu_sym = kwargs.pop("mu") + + def _get_multiplier(mu_sym): + if self.dim == 2: + return -2 / mu_sym + else: + return 1 / mu_sym if idx[0] != idx[1]: - return func(self.kernel_dict[idx]) + return _get_multiplier(mu_sym) * func(self.kernel_dict[idx], **kwargs) - return -sum(func(self.kernel_dict[(i, i)]) for i - in range(self.dim) if i != idx[0]) + return _get_multiplier(mu_sym) * \ + -sum(func(self.kernel_dict[(i, i)], **kwargs) for i + in range(self.dim) if i != idx[0]) def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel @@ -163,14 +121,14 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for the average of the two one-sided boundary limits. """ - sym_expr = np.zeros((self.dim,), dtype=object) + sym_expr = np.full((self.dim,), self.const, dtype=object) for comp in range(self.dim): for i in range(self.dim): - def func(knl): + def func(knl, **kwargs): return sym.IntG(knl, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - sym_expr[comp] += self.map_func_to_expr((comp, i), func) + qbx_forced_limit=qbx_forced_limit, **kwargs) + sym_expr[comp] += self.map_func_to_expr((comp, i), func, mu=mu_sym) return sym_expr @@ -212,13 +170,13 @@ def apply_derivative(self, deriv_dir, density_vec_sym, for comp in range(self.dim): for i in range(self.dim): - def func(knl): + def func(knl, **kwargs): return DerivativeTaker(deriv_dir).map_int_g( sym.IntG(knl, density_vec_sym[i], qbx_forced_limit=qbx_forced_limit, - mu=mu_sym)) - sym_expr[comp] += self.map_func_to_expr((comp, i), func) + **kwargs)) + sym_expr[comp] += self.map_func_to_expr((comp, i), func, mu=mu_sym) return sym_expr From 958d578cdcbe70a595cd55ebaa8e5be1d49945fa Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 9 Sep 2020 03:47:26 -0500 Subject: [PATCH 004/209] Fixes for stokes --- pytential/symbolic/stokes.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 3548573eb..fb5e5d211 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -58,7 +58,6 @@ class StokesletWrapper: def __init__(self, dim=None, use_biharmonic=True): self.use_biharmonic = use_biharmonic - self.const = 0 self.dim = dim if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -74,8 +73,6 @@ def __init__(self, dim=None, use_biharmonic=True): kernel_d1 = AxisTargetDerivative(i, _kernel) for j in range(i, dim): self.kernel_dict[(i, j)] = AxisTargetDerivative(j, kernel_d1) - if dim == 2: - self.const = 3/2 else: for i in range(dim): for j in range(i, dim): @@ -86,6 +83,13 @@ def __init__(self, dim=None, use_biharmonic=True): for j in range(i): self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] + def _get_const(self, mu_sym): + from pymbolic import var + from math import pi + if self.use_biharmonic and self.dim == 2: + return -3/(8*mu_sym*pi) + return 0 + def map_func_to_expr(self, idx, func, **kwargs): if not self.use_biharmonic: return func(self.kernel_dict[idx], **kwargs) @@ -94,7 +98,7 @@ def map_func_to_expr(self, idx, func, **kwargs): def _get_multiplier(mu_sym): if self.dim == 2: - return -2 / mu_sym + return -1 / mu_sym else: return 1 / mu_sym @@ -120,7 +124,7 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for the average of the two one-sided boundary limits. """ - sym_expr = np.full((self.dim,), self.const, dtype=object) + sym_expr = np.full((self.dim,), self._get_const(mu_sym), dtype=object) for comp in range(self.dim): for i in range(self.dim): From 0c9b1298a44cb9f99f9c56e496e73e8455df1f9d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 9 Sep 2020 03:53:57 -0500 Subject: [PATCH 005/209] Revert "Fixes for stokes" This reverts commit 958d578cdcbe70a595cd55ebaa8e5be1d49945fa. --- pytential/symbolic/stokes.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index fb5e5d211..3548573eb 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -58,6 +58,7 @@ class StokesletWrapper: def __init__(self, dim=None, use_biharmonic=True): self.use_biharmonic = use_biharmonic + self.const = 0 self.dim = dim if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -73,6 +74,8 @@ def __init__(self, dim=None, use_biharmonic=True): kernel_d1 = AxisTargetDerivative(i, _kernel) for j in range(i, dim): self.kernel_dict[(i, j)] = AxisTargetDerivative(j, kernel_d1) + if dim == 2: + self.const = 3/2 else: for i in range(dim): for j in range(i, dim): @@ -83,13 +86,6 @@ def __init__(self, dim=None, use_biharmonic=True): for j in range(i): self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] - def _get_const(self, mu_sym): - from pymbolic import var - from math import pi - if self.use_biharmonic and self.dim == 2: - return -3/(8*mu_sym*pi) - return 0 - def map_func_to_expr(self, idx, func, **kwargs): if not self.use_biharmonic: return func(self.kernel_dict[idx], **kwargs) @@ -98,7 +94,7 @@ def map_func_to_expr(self, idx, func, **kwargs): def _get_multiplier(mu_sym): if self.dim == 2: - return -1 / mu_sym + return -2 / mu_sym else: return 1 / mu_sym @@ -124,7 +120,7 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for the average of the two one-sided boundary limits. """ - sym_expr = np.full((self.dim,), self._get_const(mu_sym), dtype=object) + sym_expr = np.full((self.dim,), self.const, dtype=object) for comp in range(self.dim): for i in range(self.dim): From ea80c5a120c5ddd0f9d196b20faf45f3d09cc93a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 9 Sep 2020 03:54:01 -0500 Subject: [PATCH 006/209] Fix adding the constant --- pytential/symbolic/stokes.py | 81 ++++++++++++++++-------------------- test/test_stokes.py | 2 +- 2 files changed, 38 insertions(+), 45 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 3548573eb..400223c2a 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -58,52 +58,53 @@ class StokesletWrapper: def __init__(self, dim=None, use_biharmonic=True): self.use_biharmonic = use_biharmonic - self.const = 0 self.dim = dim if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") self.kernel_dict = {} - if self.use_biharmonic: - # Note that for 3D, the following holds, - # $K_{i, j} = \nabla \nabla + \mathrm{I} \nabla^2 r $ - # where $r$ is the fundamental solution to - _kernel = BiharmonicKernel(dim=dim) - for i in range(dim): - kernel_d1 = AxisTargetDerivative(i, _kernel) - for j in range(i, dim): - self.kernel_dict[(i, j)] = AxisTargetDerivative(j, kernel_d1) - if dim == 2: - self.const = 3/2 - else: + self.base_kernel = BiharmonicKernel(dim=dim) + + if not use_biharmonic: for i in range(dim): for j in range(i, dim): self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, jcomp=j) - for i in range(dim): - for j in range(i): - self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] + for i in range(dim): + for j in range(i): + self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] + + def _get_stokeslet_int_g(self, icomp, jcomp, density, mu_sym, qbx_forced_limit, + deriv_dirs): + + def func(knl): + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + + res = sym.IntG(knl, density, + qbx_forced_limit=qbx_forced_limit, mu=mu_sym) + + return res + + deriv_dirs.extend([icomp, jcomp]) - def map_func_to_expr(self, idx, func, **kwargs): if not self.use_biharmonic: - return func(self.kernel_dict[idx], **kwargs) + return func(self.base_kernel) - mu_sym = kwargs.pop("mu") + mult = -1 / mu_sym - def _get_multiplier(mu_sym): - if self.dim == 2: - return -2 / mu_sym - else: - return 1 / mu_sym + if icomp != jcomp: + return mult * func(self.base_kernel) - if idx[0] != idx[1]: - return _get_multiplier(mu_sym) * func(self.kernel_dict[idx], **kwargs) + const = 0 + if self.dim == 2 and not deriv_dirs: + from math import pi + const = -3/(8*pi) - return _get_multiplier(mu_sym) * \ - -sum(func(self.kernel_dict[(i, i)], **kwargs) for i - in range(self.dim) if i != idx[0]) + return -mult * (sum(func(self.base_kernel) for i + in range(self.dim) if i != icomp) + const * density) def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel @@ -120,14 +121,12 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for the average of the two one-sided boundary limits. """ - sym_expr = np.full((self.dim,), self.const, dtype=object) + sym_expr = np.zeros((self.dim,), dtype=object) for comp in range(self.dim): for i in range(self.dim): - def func(knl, **kwargs): - return sym.IntG(knl, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, **kwargs) - sym_expr[comp] += self.map_func_to_expr((comp, i), func, mu=mu_sym) + sym_expr[comp] += self._get_stokeslet_int_g(comp, i, + density_vec_sym[i], mu_sym, qbx_forced_limit, deriv_dirs=[]) return sym_expr @@ -163,19 +162,13 @@ def apply_derivative(self, deriv_dir, density_vec_sym, for the average of the two one-sided boundary limits. """ - from pytential.symbolic.mappers import DerivativeTaker - - sym_expr = np.zeros((self.dim,), dtype=object) + sym_expr = self.apply(density_vec_sym, mu_sym, qbx_forced_limit) for comp in range(self.dim): for i in range(self.dim): - def func(knl, **kwargs): - return DerivativeTaker(deriv_dir).map_int_g( - sym.IntG(knl, - density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, - **kwargs)) - sym_expr[comp] += self.map_func_to_expr((comp, i), func, mu=mu_sym) + sym_expr[comp] += self._get_stokeslet_int_g(comp, i, + density_vec_sym[i], mu_sym, qbx_forced_limit, + deriv_dirs=[deriv_dir]) return sym_expr diff --git a/test/test_stokes.py b/test/test_stokes.py index 676f9d059..f53caadc2 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -44,7 +44,7 @@ def run_exterior_stokes_2d(ctx_factory, nelements, mesh_order=4, target_order=4, qbx_order=4, - fmm_order=False, # FIXME: FMM is slower than direct eval + fmm_order=7, # FIXME: FMM is slower than direct eval mu=1, circle_rad=1.5, visualize=False): # This program tests an exterior Stokes flow in 2D using the From 55b1430c9ab7f9987274a54c0c6e195c0ee2a7e1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Sep 2020 20:16:54 -0500 Subject: [PATCH 007/209] Fix deriv_dirs --- pytential/symbolic/stokes.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 400223c2a..31b2e1d51 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -79,7 +79,7 @@ def __init__(self, dim=None, use_biharmonic=True): def _get_stokeslet_int_g(self, icomp, jcomp, density, mu_sym, qbx_forced_limit, deriv_dirs): - def func(knl): + def func(knl, deriv_dirs): for deriv_dir in deriv_dirs: knl = AxisTargetDerivative(deriv_dir, knl) @@ -88,23 +88,23 @@ def func(knl): return res - deriv_dirs.extend([icomp, jcomp]) - if not self.use_biharmonic: - return func(self.base_kernel) + return func(self.base_kernel, deriv_dirs=deriv_dirs) mult = -1 / mu_sym if icomp != jcomp: - return mult * func(self.base_kernel) + return mult * func(self.base_kernel, + deriv_dirs=(deriv_dirs + [icomp, jcomp])) const = 0 if self.dim == 2 and not deriv_dirs: from math import pi const = -3/(8*pi) - return -mult * (sum(func(self.base_kernel) for i - in range(self.dim) if i != icomp) + const * density) + return -mult * (sum(func(self.base_kernel, + deriv_dirs=(deriv_dirs + [i, i])) for i in range(self.dim) + if i != icomp) + const * density) def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel From 33c872294e9a625deeb62e5ac963e13980f5c212 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 11 Sep 2020 14:00:10 -0500 Subject: [PATCH 008/209] remove slow test marker --- test/test_stokes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index f53caadc2..4c8621759 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -299,7 +299,6 @@ def get_obj_array(obj_array): return h_max, l2_err -@pytest.mark.slowtest def test_exterior_stokes_2d(ctx_factory, qbx_order=3): from pytools.convergence import EOCRecorder eoc_rec = EOCRecorder() From 90b507ddc387a20e509bcf650c194cca13457875 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 11 Sep 2020 15:01:16 -0500 Subject: [PATCH 009/209] Add a comment and fix IntG --- pytential/symbolic/stokes.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 31b2e1d51..f17e8061f 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -79,18 +79,42 @@ def __init__(self, dim=None, use_biharmonic=True): def _get_stokeslet_int_g(self, icomp, jcomp, density, mu_sym, qbx_forced_limit, deriv_dirs): + """ + Returns the Integral of the Stokeslet kernel given by `icomp` and `jcomp` + and its derivatives. + Note that, + + Stokeslet(icomp, jcomp) + = -1/mu_sym d/(d x_icomp) d/(d x_jcomp) BiharmonicKernel + + for icomp != jcomp. Also note that for 3D, + + Stokeslet(i, i) + = -1/mu_sym sum(-d^2/(dx_j^2 BiharmonicKernel for j != i). + + For 2D, + + Stokeslet(i, i) + = -1/mu_sym (sum(d^2/dx_j^2 BiharmonicKernel for j != i) + -3/(8*pi)). + """ + + if not self.use_biharmonic: + knl = self.kernel_dict[(icomp, jcomp)] + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + return sym.IntG(knl, density, + qbx_forced_limit=qbx_forced_limit, mu=mu_sym) + + def func(knl, deriv_dirs): for deriv_dir in deriv_dirs: knl = AxisTargetDerivative(deriv_dir, knl) res = sym.IntG(knl, density, - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - + qbx_forced_limit=qbx_forced_limit) return res - if not self.use_biharmonic: - return func(self.base_kernel, deriv_dirs=deriv_dirs) - mult = -1 / mu_sym if icomp != jcomp: From 30a73a929af1c6ddfcd0d991b5a4c5d418c96958 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 11 Sep 2020 19:54:29 -0500 Subject: [PATCH 010/209] Use sym.integral --- pytential/symbolic/stokes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index f17e8061f..64f458e69 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -106,7 +106,6 @@ def _get_stokeslet_int_g(self, icomp, jcomp, density, mu_sym, qbx_forced_limit, return sym.IntG(knl, density, qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - def func(knl, deriv_dirs): for deriv_dir in deriv_dirs: knl = AxisTargetDerivative(deriv_dir, knl) @@ -124,11 +123,11 @@ def func(knl, deriv_dirs): const = 0 if self.dim == 2 and not deriv_dirs: from math import pi - const = -3/(8*pi) + const = -3/(8*pi) * sym.integral(self.dim, self.dim-1, density) return -mult * (sum(func(self.base_kernel, deriv_dirs=(deriv_dirs + [i, i])) for i in range(self.dim) - if i != icomp) + const * density) + if i != icomp) + const) def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel From 8fb6142db3b436cc7616222de12ce4be25faa6b0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 15 Sep 2020 14:36:21 -0500 Subject: [PATCH 011/209] Pass source as dofdesc --- pytential/symbolic/stokes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 64f458e69..ede26b89d 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -123,7 +123,9 @@ def func(knl, deriv_dirs): const = 0 if self.dim == 2 and not deriv_dirs: from math import pi - const = -3/(8*pi) * sym.integral(self.dim, self.dim-1, density) + from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE + const = -3/(8*pi) * sym.integral(self.dim, self.dim-1, density, + dofdesc=as_dofdesc(DEFAULT_SOURCE)) return -mult * (sum(func(self.base_kernel, deriv_dirs=(deriv_dirs + [i, i])) for i in range(self.dim) From 3d18a3f903120283b37a7524ff56ed9629d273cb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 22 Sep 2020 21:44:45 -0500 Subject: [PATCH 012/209] tag the mean with DEFUALT_SOURCE --- test/test_stokes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 4c8621759..12ce7997b 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -118,11 +118,13 @@ def outside_circle(test_points, radius): # {{{ describe bvp from pytential.symbolic.stokes import StressletWrapper, StokesletWrapper + from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE dim = 2 cse = sym.cse sigma_sym = sym.make_sym_vector("sigma", dim) - meanless_sigma_sym = cse(sigma_sym - sym.mean(2, 1, sigma_sym)) + meanless_sigma_sym = cse(sigma_sym - + sym.mean(2, 1, sigma_sym, dofdesc=as_dofdesc(DEFAULT_SOURCE))) int_sigma = sym.Ones() * sym.integral(2, 1, sigma_sym) nvec_sym = sym.make_sym_vector("normal", dim) From d534088dc9fe8218dd674c9fdc81f861e81f53c7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 22 Sep 2020 23:38:15 -0500 Subject: [PATCH 013/209] Fix formatting --- test/test_stokes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 12ce7997b..234c3c72c 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -23,7 +23,6 @@ import numpy as np import pyopencl as cl -import pytest from meshmode.array_context import PyOpenCLArrayContext from meshmode.discretization import Discretization @@ -123,8 +122,8 @@ def outside_circle(test_points, radius): cse = sym.cse sigma_sym = sym.make_sym_vector("sigma", dim) - meanless_sigma_sym = cse(sigma_sym - - sym.mean(2, 1, sigma_sym, dofdesc=as_dofdesc(DEFAULT_SOURCE))) + meanless_sigma_sym = cse(sigma_sym + - sym.mean(2, 1, sigma_sym, dofdesc=as_dofdesc(DEFAULT_SOURCE))) int_sigma = sym.Ones() * sym.integral(2, 1, sigma_sym) nvec_sym = sym.make_sym_vector("normal", dim) From 764e18ec03ac31f8b6abe3a1fbe37bf91ed2e5c0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 28 Sep 2020 19:31:45 -0500 Subject: [PATCH 014/209] Automatically figure out the derivative relation --- pytential/symbolic/pde_system_utils.py | 101 +++++++++++++++++++++++++ pytential/symbolic/stokes.py | 57 ++++++++------ 2 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 pytential/symbolic/pde_system_utils.py diff --git a/pytential/symbolic/pde_system_utils.py b/pytential/symbolic/pde_system_utils.py new file mode 100644 index 000000000..85ac926b6 --- /dev/null +++ b/pytential/symbolic/pde_system_utils.py @@ -0,0 +1,101 @@ +import numpy as np + +from sumpy.symbolic import Matrix, make_sym_vector, sym + +from pytools import ( + generate_nonnegative_integer_tuples_summing_to_at_most + as gnitstam) + + +def _chop(expr, tol): + nums = expr.atoms(sym.Number) + replace_dict = {} + for num in nums: + if abs(num) < tol: + replace_dict[num] = 0 + else: + new_num = float(num) + if abs((int(new_num) - new_num)/new_num) < tol: + new_num = int(new_num) + replace_dict[num] = new_num + return expr.xreplace(replace_dict) + + +def _n(expr): + from sumpy.symbolic import USE_SYMENGINE + if USE_SYMENGINE: + # 100 bits + return expr.n(prec=100) + else: + # 30 decimal places + return expr.n(n=30) + + +def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): + dim = base_kernel.dim + + mis = sorted(gnitstam(order, dim), key=sum) + + # (-1, -1, -1) represent a constant + mis.append((-1, -1, -1)) + + rand = np.random.randint(1, 100, (dim, len(mis))) + sym_vec = make_sym_vector("d", dim) + + base_expr = base_kernel.get_expression(sym_vec) + + mat = [] + for rand_vec_idx in range(rand.shape[1]): + row = [] + for mi in mis[:-1]: + expr = base_expr + for var_idx, nderivs in enumerate(mi): + if nderivs == 0: + continue + expr = expr.diff(sym_vec[var_idx], nderivs) + replace_dict = dict( + (k, v) for k, v in zip(sym_vec, rand[:, rand_vec_idx]) + ) + eval_expr = _n(expr.xreplace(replace_dict)) + row.append(eval_expr) + row.append(1) + mat.append(row) + + mat = Matrix(mat) + res = [] + + for kernel in kernels: + expr = kernel.get_expression(sym_vec) + vec = [] + for a in rand.T: + vec.append(_n(expr.xreplace(dict((k, v) for k, v in zip(sym_vec, a))))) + vec = Matrix(vec) + result = [] + const = 0 + if verbose: + print(kernel, end=' = ') + for i, coeff in enumerate(mat.solve(vec)): + coeff = _chop(coeff, tol) + if coeff == 0: + continue + if mis[i] != (-1, -1, -1): + coeff *= kernel.get_global_scaling_const() + coeff /= base_kernel.get_global_scaling_const() + result.append((mis[i], coeff)) + if verbose: + print(f"{base_kernel}.diff({mis[i]})*{coeff}", end=" + ") + else: + const = coeff * kernel.get_global_scaling_const() + if verbose: + print(const) + res.append((const, result)) + + return res + + +if __name__ == "__main__": + from sumpy.kernel import StokesletKernel, BiharmonicKernel, StressletKernel + base_kernel = BiharmonicKernel(2) + kernels = [StokesletKernel(2, 0, 1), StokesletKernel(2, 0, 0)] + kernels = [StressletKernel(2, 0, 1, 0)] + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=3) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index ede26b89d..6d1844145 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -66,15 +66,22 @@ def __init__(self, dim=None, use_biharmonic=True): self.base_kernel = BiharmonicKernel(dim=dim) - if not use_biharmonic: - for i in range(dim): - for j in range(i, dim): - self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, - jcomp=j) - - for i in range(dim): - for j in range(i): - self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] + for i in range(dim): + for j in range(i, dim): + self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, + jcomp=j) + + for i in range(dim): + for j in range(i): + self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] + + if self.use_biharmonic: + from pytential.symbolic.pde_system_utils import get_deriv_relation + results = get_deriv_relation(list(self.kernel_dict.values()), + self.base_kernel, tol=1e-10, order=2) + self.deriv_relation_dict = {} + for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): + self.deriv_relation_dict[idx] = deriv_eq def _get_stokeslet_int_g(self, icomp, jcomp, density, mu_sym, qbx_forced_limit, deriv_dirs): @@ -114,22 +121,22 @@ def func(knl, deriv_dirs): qbx_forced_limit=qbx_forced_limit) return res - mult = -1 / mu_sym - - if icomp != jcomp: - return mult * func(self.base_kernel, - deriv_dirs=(deriv_dirs + [icomp, jcomp])) - - const = 0 - if self.dim == 2 and not deriv_dirs: - from math import pi - from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE - const = -3/(8*pi) * sym.integral(self.dim, self.dim-1, density, - dofdesc=as_dofdesc(DEFAULT_SOURCE)) - - return -mult * (sum(func(self.base_kernel, - deriv_dirs=(deriv_dirs + [i, i])) for i in range(self.dim) - if i != icomp) + const) + deriv_relation = self.deriv_relation_dict[(icomp, jcomp)] + from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE + from sumpy.symbolic import SympyToPymbolicMapper + sympy_conv = SympyToPymbolicMapper() + const = sympy_conv(deriv_relation[0]) + const *= sym.integral(self.dim, self.dim-1, density, + dofdesc=as_dofdesc(DEFAULT_SOURCE)) + + result = 0 + for mi, coeff in deriv_relation[1]: + deriv_dirs = list(deriv_dirs) + for idx, val in enumerate(mi): + deriv_dirs.extend([idx]*val) + result += func(self.base_kernel, deriv_dirs) * sympy_conv(coeff) + + return result def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel From 05e1d9dbafad41d40d7c88cc4819375882e0c9a8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 28 Sep 2020 19:39:51 -0500 Subject: [PATCH 015/209] fix formatting --- pytential/symbolic/pde_system_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde_system_utils.py b/pytential/symbolic/pde_system_utils.py index 85ac926b6..402e4b76f 100644 --- a/pytential/symbolic/pde_system_utils.py +++ b/pytential/symbolic/pde_system_utils.py @@ -73,7 +73,7 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): result = [] const = 0 if verbose: - print(kernel, end=' = ') + print(kernel, end=" = ") for i, coeff in enumerate(mat.solve(vec)): coeff = _chop(coeff, tol) if coeff == 0: From edf000cfb5f1bb1d3afe69d25328f1ffde41c20c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 28 Sep 2020 19:55:11 -0500 Subject: [PATCH 016/209] convert in get_deriv_relation --- pytential/symbolic/pde_system_utils.py | 10 +++++----- pytential/symbolic/stokes.py | 6 ++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/pde_system_utils.py b/pytential/symbolic/pde_system_utils.py index 402e4b76f..73687932e 100644 --- a/pytential/symbolic/pde_system_utils.py +++ b/pytential/symbolic/pde_system_utils.py @@ -1,7 +1,6 @@ import numpy as np -from sumpy.symbolic import Matrix, make_sym_vector, sym - +from sumpy.symbolic import Matrix, make_sym_vector, sym, SympyToPymbolicMapper from pytools import ( generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) @@ -43,6 +42,7 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): sym_vec = make_sym_vector("d", dim) base_expr = base_kernel.get_expression(sym_vec) + sympy_conv = SympyToPymbolicMapper() mat = [] for rand_vec_idx in range(rand.shape[1]): @@ -81,11 +81,11 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): if mis[i] != (-1, -1, -1): coeff *= kernel.get_global_scaling_const() coeff /= base_kernel.get_global_scaling_const() - result.append((mis[i], coeff)) + result.append((mis[i], sympy_conv(coeff))) if verbose: print(f"{base_kernel}.diff({mis[i]})*{coeff}", end=" + ") else: - const = coeff * kernel.get_global_scaling_const() + const = sympy_conv(coeff * kernel.get_global_scaling_const()) if verbose: print(const) res.append((const, result)) @@ -98,4 +98,4 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): base_kernel = BiharmonicKernel(2) kernels = [StokesletKernel(2, 0, 1), StokesletKernel(2, 0, 0)] kernels = [StressletKernel(2, 0, 1, 0)] - get_deriv_relation(kernels, base_kernel, tol=1e-10, order=3) + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=3, verbose=True) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 6d1844145..cfab5b6b7 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -123,9 +123,7 @@ def func(knl, deriv_dirs): deriv_relation = self.deriv_relation_dict[(icomp, jcomp)] from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE - from sumpy.symbolic import SympyToPymbolicMapper - sympy_conv = SympyToPymbolicMapper() - const = sympy_conv(deriv_relation[0]) + const = deriv_relation[0] const *= sym.integral(self.dim, self.dim-1, density, dofdesc=as_dofdesc(DEFAULT_SOURCE)) @@ -134,7 +132,7 @@ def func(knl, deriv_dirs): deriv_dirs = list(deriv_dirs) for idx, val in enumerate(mi): deriv_dirs.extend([idx]*val) - result += func(self.base_kernel, deriv_dirs) * sympy_conv(coeff) + result += func(self.base_kernel, deriv_dirs) * coeff return result From ee2fb45934e423224b70ab2d130232ee1f32f581 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 28 Sep 2020 21:05:28 -0500 Subject: [PATCH 017/209] increase fmm_order --- test/test_stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 234c3c72c..9e6c83c6e 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -43,7 +43,7 @@ def run_exterior_stokes_2d(ctx_factory, nelements, mesh_order=4, target_order=4, qbx_order=4, - fmm_order=7, # FIXME: FMM is slower than direct eval + fmm_order=10, # FIXME: FMM is slower than direct eval mu=1, circle_rad=1.5, visualize=False): # This program tests an exterior Stokes flow in 2D using the From d1c53e0fe282e77793f222ca3341cb64dc3902bb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 28 Sep 2020 21:44:13 -0500 Subject: [PATCH 018/209] Clean up stressletwrapper and use biharmonic --- pytential/symbolic/pde_system_utils.py | 22 ++ pytential/symbolic/stokes.py | 273 ++++++++++--------------- test/test_stokes.py | 2 +- 3 files changed, 128 insertions(+), 169 deletions(-) diff --git a/pytential/symbolic/pde_system_utils.py b/pytential/symbolic/pde_system_utils.py index 73687932e..446c434e5 100644 --- a/pytential/symbolic/pde_system_utils.py +++ b/pytential/symbolic/pde_system_utils.py @@ -1,3 +1,25 @@ +__copyright__ = "Copyright (C) 2020 Isuru Fernando" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + import numpy as np from sumpy.symbolic import Matrix, make_sym_vector, sym, SympyToPymbolicMapper diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index cfab5b6b7..a03f5d5e7 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,7 +27,56 @@ AxisTargetDerivative, BiharmonicKernel) -class StokesletWrapper: +class StokesletWrapperBase: + """A base class for StokesletWrapper and StressletWrapper + + """ + def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, + deriv_dirs): + + """ + Returns the Integral of the Stokeslet/Stresslet kernel given by `icomp` + and `jcomp` and its derivatives. + For instance, + + Stokeslet(icomp, jcomp) + = -1/mu_sym d/(d x_icomp) d/(d x_jcomp) BiharmonicKernel + + for icomp != jcomp. + """ + + if not self.use_biharmonic: + knl = self.kernel_dict[idx] + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + return sym.IntG(knl, density, + qbx_forced_limit=qbx_forced_limit, mu=mu_sym) + + def func(knl, deriv_dirs): + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + + res = sym.IntG(knl, density, + qbx_forced_limit=qbx_forced_limit) + return res + + deriv_relation = self.deriv_relation_dict[idx] + from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE + const = deriv_relation[0] + const *= sym.integral(self.dim, self.dim-1, density, + dofdesc=as_dofdesc(DEFAULT_SOURCE)) + + result = 0 + for mi, coeff in deriv_relation[1]: + deriv_dirs = list(deriv_dirs) + for idx, val in enumerate(mi): + deriv_dirs.extend([idx]*val) + result += func(self.base_kernel, deriv_dirs) * coeff + + return result + + +class StokesletWrapper(StokesletWrapperBase): """ Wrapper class for the Stokeslet kernel. This class is meant to shield the user from the messiness of writing @@ -83,59 +132,6 @@ def __init__(self, dim=None, use_biharmonic=True): for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): self.deriv_relation_dict[idx] = deriv_eq - def _get_stokeslet_int_g(self, icomp, jcomp, density, mu_sym, qbx_forced_limit, - deriv_dirs): - - """ - Returns the Integral of the Stokeslet kernel given by `icomp` and `jcomp` - and its derivatives. - Note that, - - Stokeslet(icomp, jcomp) - = -1/mu_sym d/(d x_icomp) d/(d x_jcomp) BiharmonicKernel - - for icomp != jcomp. Also note that for 3D, - - Stokeslet(i, i) - = -1/mu_sym sum(-d^2/(dx_j^2 BiharmonicKernel for j != i). - - For 2D, - - Stokeslet(i, i) - = -1/mu_sym (sum(d^2/dx_j^2 BiharmonicKernel for j != i) - -3/(8*pi)). - """ - - if not self.use_biharmonic: - knl = self.kernel_dict[(icomp, jcomp)] - for deriv_dir in deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - return sym.IntG(knl, density, - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - - def func(knl, deriv_dirs): - for deriv_dir in deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - - res = sym.IntG(knl, density, - qbx_forced_limit=qbx_forced_limit) - return res - - deriv_relation = self.deriv_relation_dict[(icomp, jcomp)] - from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE - const = deriv_relation[0] - const *= sym.integral(self.dim, self.dim-1, density, - dofdesc=as_dofdesc(DEFAULT_SOURCE)) - - result = 0 - for mi, coeff in deriv_relation[1]: - deriv_dirs = list(deriv_dirs) - for idx, val in enumerate(mi): - deriv_dirs.extend([idx]*val) - result += func(self.base_kernel, deriv_dirs) * coeff - - return result - def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating Stokeslet kernel @@ -155,7 +151,7 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): for comp in range(self.dim): for i in range(self.dim): - sym_expr[comp] += self._get_stokeslet_int_g(comp, i, + sym_expr[comp] += self.get_int_g((comp, i), density_vec_sym[i], mu_sym, qbx_forced_limit, deriv_dirs=[]) return sym_expr @@ -196,7 +192,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, for comp in range(self.dim): for i in range(self.dim): - sym_expr[comp] += self._get_stokeslet_int_g(comp, i, + sym_expr[comp] += self.get_int_g((comp, i), density_vec_sym[i], mu_sym, qbx_forced_limit, deriv_dirs=[deriv_dir]) @@ -231,34 +227,21 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, for the average of the two one-sided boundary limits. """ - import itertools - sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = StressletWrapper(dim=self.dim) for comp in range(self.dim): - - # Start variable count for kernel with 1 for the requested result - # component - base_count = np.zeros(self.dim, dtype=np.int) - base_count[comp] += 1 - - for i, j in itertools.product(range(self.dim), range(self.dim)): - var_ctr = base_count.copy() - var_ctr[i] += 1 - var_ctr[j] += 1 - ctr_key = tuple(var_ctr) - - sym_expr[comp] += dir_vec_sym[i] * sym.IntG( - stresslet_obj.kernel_dict[ctr_key], - density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym) + for i in range(self.dim): + for j in range(self.dim): + sym_expr[comp] += dir_vec_sym[i] * \ + stresslet_obj.get_int_g((comp, i, j), + density_vec_sym[j], + mu_sym, qbx_forced_limit, deriv_dirs=[]) return sym_expr -class StressletWrapper: +class StressletWrapper(StokesletWrapperBase): """ Wrapper class for the Stresslet kernel. This class is meant to shield the user from the messiness of writing @@ -287,33 +270,37 @@ class StressletWrapper: """ - def __init__(self, dim=None): - + def __init__(self, dim=None, use_biharmonic=False): + self.use_biharmonic = use_biharmonic self.dim = dim - if dim == 2: - self.kernel_dict = { - (3, 0): StressletKernel(dim=2, icomp=0, jcomp=0, kcomp=0), - (2, 1): StressletKernel(dim=2, icomp=0, jcomp=0, kcomp=1), - (1, 2): StressletKernel(dim=2, icomp=0, jcomp=1, kcomp=1), - (0, 3): StressletKernel(dim=2, icomp=1, jcomp=1, kcomp=1) - } - - elif dim == 3: - self.kernel_dict = { - (3, 0, 0): StressletKernel(dim=3, icomp=0, jcomp=0, kcomp=0), - (2, 1, 0): StressletKernel(dim=3, icomp=0, jcomp=0, kcomp=1), - (2, 0, 1): StressletKernel(dim=3, icomp=0, jcomp=0, kcomp=2), - (1, 2, 0): StressletKernel(dim=3, icomp=0, jcomp=1, kcomp=1), - (1, 1, 1): StressletKernel(dim=3, icomp=0, jcomp=1, kcomp=2), - (1, 0, 2): StressletKernel(dim=3, icomp=0, jcomp=2, kcomp=2), - (0, 3, 0): StressletKernel(dim=3, icomp=1, jcomp=1, kcomp=1), - (0, 2, 1): StressletKernel(dim=3, icomp=1, jcomp=1, kcomp=2), - (0, 1, 2): StressletKernel(dim=3, icomp=1, jcomp=2, kcomp=2), - (0, 0, 3): StressletKernel(dim=3, icomp=2, jcomp=2, kcomp=2) - } - - else: - raise ValueError("unsupported dimension given to StressletWrapper") + if not (dim == 3 or dim == 2): + raise ValueError("unsupported dimension given to StokesletWrapper") + + self.kernel_dict = {} + + self.base_kernel = BiharmonicKernel(dim=dim) + + for i in range(dim): + for j in range(i, dim): + for k in range(j, dim): + self.kernel_dict[(i, j, k)] = StressletKernel(dim=dim, icomp=i, + jcomp=j, kcomp=k) + + for i in range(dim): + for j in range(dim): + for k in range(dim): + if (i, j, k) in self.kernel_dict: + continue + s = tuple(sorted([i, j, k])) + self.kernel_dict[(i, j, k)] = self.kernel_dict[s] + + if self.use_biharmonic: + from pytential.symbolic.pde_system_utils import get_deriv_relation + results = get_deriv_relation(list(self.kernel_dict.values()), + self.base_kernel, tol=1e-10, order=3) + self.deriv_relation_dict = {} + for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): + self.deriv_relation_dict[idx] = deriv_eq def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): """ Symbolic expressions for integrating stresslet kernel @@ -331,35 +318,14 @@ def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): for the average of the two one-sided boundary limits. """ - import itertools - - sym_expr = np.empty((self.dim,), dtype=object) + sym_expr = np.zeros((self.dim,), dtype=object) for comp in range(self.dim): - - # Start variable count for kernel with 1 for the requested result - # component - base_count = np.zeros(self.dim, dtype=np.int) - base_count[comp] += 1 - - for i, j in itertools.product(range(self.dim), range(self.dim)): - var_ctr = base_count.copy() - var_ctr[i] += 1 - var_ctr[j] += 1 - ctr_key = tuple(var_ctr) - - if i + j < 1: - sym_expr[comp] = sym.IntG( - self.kernel_dict[ctr_key], - dir_vec_sym[i] * density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - - else: - sym_expr[comp] = sym_expr[comp] + sym.IntG( - self.kernel_dict[ctr_key], - dir_vec_sym[i] * density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym) + for i in range(self.dim): + for j in range(self.dim): + sym_expr[comp] += self.get_int_g((comp, i, j), + dir_vec_sym[i] * density_vec_sym[j], + mu_sym, qbx_forced_limit, deriv_dirs=[]) return sym_expr @@ -372,20 +338,14 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit) factor = (2. * mu_sym) - for i, j in itertools.product(range(self.dim), range(self.dim)): + sym_expr = 0 - if i + j < 1: - sym_expr = factor * DerivativeTaker(i).map_int_g( - DerivativeTaker(j).map_int_g( - sym.S(kernel, density_vec_sym[i] * dir_vec_sym[j], - qbx_forced_limit=qbx_forced_limit))) - else: - sym_expr = sym_expr + ( - factor * DerivativeTaker(i).map_int_g( + for i, j in itertools.product(range(self.dim), range(self.dim)): + sym_expr += factor * DerivativeTaker(i).map_int_g( DerivativeTaker(j).map_int_g( sym.S(kernel, density_vec_sym[i] * dir_vec_sym[j], - qbx_forced_limit=qbx_forced_limit)))) + qbx_forced_limit=qbx_forced_limit))) return sym_expr @@ -408,37 +368,14 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, for the average of the two one-sided boundary limits. """ - import itertools - from pytential.symbolic.mappers import DerivativeTaker - - sym_expr = np.empty((self.dim,), dtype=object) + sym_expr = np.zeros((self.dim,), dtype=object) for comp in range(self.dim): - - # Start variable count for kernel with 1 for the requested result - # component - base_count = np.zeros(self.dim, dtype=np.int) - base_count[comp] += 1 - - for i, j in itertools.product(range(self.dim), range(self.dim)): - var_ctr = base_count.copy() - var_ctr[i] += 1 - var_ctr[j] += 1 - ctr_key = tuple(var_ctr) - - if i + j < 1: - sym_expr[comp] = DerivativeTaker(deriv_dir).map_int_g( - sym.IntG(self.kernel_dict[ctr_key], - dir_vec_sym[i] * density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym)) - - else: - sym_expr[comp] = sym_expr[comp] + DerivativeTaker( - deriv_dir).map_int_g( - sym.IntG(self.kernel_dict[ctr_key], - dir_vec_sym[i] * density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym)) + for i in range(self.dim): + for j in range(self.dim): + sym_expr[comp] += self.get_int_g((comp, i, j), + dir_vec_sym[i] * density_vec_sym[j], + mu_sym, qbx_forced_limit, deriv_dirs=[deriv_dir]) return sym_expr diff --git a/test/test_stokes.py b/test/test_stokes.py index 9e6c83c6e..ef9902503 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -43,7 +43,7 @@ def run_exterior_stokes_2d(ctx_factory, nelements, mesh_order=4, target_order=4, qbx_order=4, - fmm_order=10, # FIXME: FMM is slower than direct eval + fmm_order=12, # FIXME: FMM is slower than direct eval mu=1, circle_rad=1.5, visualize=False): # This program tests an exterior Stokes flow in 2D using the From 07067ca9cf2467129535faa8b86213313209da65 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 28 Sep 2020 22:52:28 -0500 Subject: [PATCH 019/209] refator and make use_biharmonic default --- .../{pde_system_utils.py => pde/system_utils.py} | 0 pytential/symbolic/stokes.py | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) rename pytential/symbolic/{pde_system_utils.py => pde/system_utils.py} (100%) diff --git a/pytential/symbolic/pde_system_utils.py b/pytential/symbolic/pde/system_utils.py similarity index 100% rename from pytential/symbolic/pde_system_utils.py rename to pytential/symbolic/pde/system_utils.py diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index a03f5d5e7..8bd74f41d 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -125,7 +125,7 @@ def __init__(self, dim=None, use_biharmonic=True): self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] if self.use_biharmonic: - from pytential.symbolic.pde_system_utils import get_deriv_relation + from pytential.symbolic.pde.system_utils import get_deriv_relation results = get_deriv_relation(list(self.kernel_dict.values()), self.base_kernel, tol=1e-10, order=2) self.deriv_relation_dict = {} @@ -228,7 +228,8 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, """ sym_expr = np.zeros((self.dim,), dtype=object) - stresslet_obj = StressletWrapper(dim=self.dim) + stresslet_obj = StressletWrapper(dim=self.dim, + use_biharmonic=self.use_biharmonic) for comp in range(self.dim): for i in range(self.dim): @@ -270,7 +271,7 @@ class StressletWrapper(StokesletWrapperBase): """ - def __init__(self, dim=None, use_biharmonic=False): + def __init__(self, dim=None, use_biharmonic=True): self.use_biharmonic = use_biharmonic self.dim = dim if not (dim == 3 or dim == 2): @@ -295,7 +296,7 @@ def __init__(self, dim=None, use_biharmonic=False): self.kernel_dict[(i, j, k)] = self.kernel_dict[s] if self.use_biharmonic: - from pytential.symbolic.pde_system_utils import get_deriv_relation + from pytential.symbolic.pde.system_utils import get_deriv_relation results = get_deriv_relation(list(self.kernel_dict.values()), self.base_kernel, tol=1e-10, order=3) self.deriv_relation_dict = {} From 5efcba94e1bc35782827ad30e7c854be98b9af0e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 6 Dec 2020 13:46:30 -0600 Subject: [PATCH 020/209] Fix stokes stresslet --- pytential/symbolic/stokes.py | 8 ++++---- test/test_stokes.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 8bd74f41d..e1a014f10 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -66,12 +66,12 @@ def func(knl, deriv_dirs): const *= sym.integral(self.dim, self.dim-1, density, dofdesc=as_dofdesc(DEFAULT_SOURCE)) - result = 0 + result = const for mi, coeff in deriv_relation[1]: - deriv_dirs = list(deriv_dirs) + new_deriv_dirs = list(deriv_dirs) for idx, val in enumerate(mi): - deriv_dirs.extend([idx]*val) - result += func(self.base_kernel, deriv_dirs) * coeff + new_deriv_dirs.extend([idx]*val) + result += func(self.base_kernel, new_deriv_dirs) * coeff return result diff --git a/test/test_stokes.py b/test/test_stokes.py index ef9902503..e7635e966 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -43,7 +43,7 @@ def run_exterior_stokes_2d(ctx_factory, nelements, mesh_order=4, target_order=4, qbx_order=4, - fmm_order=12, # FIXME: FMM is slower than direct eval + fmm_order=10, mu=1, circle_rad=1.5, visualize=False): # This program tests an exterior Stokes flow in 2D using the From b404b023224fee173954092e7f320cfd3ab9a8ae Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 6 Dec 2020 14:05:56 -0600 Subject: [PATCH 021/209] make pylint happy --- pytential/symbolic/pde/system_utils.py | 10 +++++----- pytential/symbolic/stokes.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 446c434e5..ccb1cb282 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -22,7 +22,7 @@ import numpy as np -from sumpy.symbolic import Matrix, make_sym_vector, sym, SympyToPymbolicMapper +from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper from pytools import ( generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) @@ -83,15 +83,15 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): row.append(1) mat.append(row) - mat = Matrix(mat) + mat = sym.Matrix(mat) res = [] for kernel in kernels: expr = kernel.get_expression(sym_vec) vec = [] - for a in rand.T: - vec.append(_n(expr.xreplace(dict((k, v) for k, v in zip(sym_vec, a))))) - vec = Matrix(vec) + for i in range(len(mis)): + vec.append(_n(expr.xreplace(dict((k, v) for k, v in zip(sym_vec, rand[:, i]))))) + vec = sym.Matrix(vec) result = [] const = 0 if verbose: diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index e1a014f10..78b2934cd 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,7 +27,7 @@ AxisTargetDerivative, BiharmonicKernel) -class StokesletWrapperBase: +class StokesletWrapperMixin: """A base class for StokesletWrapper and StressletWrapper """ @@ -76,7 +76,7 @@ def func(knl, deriv_dirs): return result -class StokesletWrapper(StokesletWrapperBase): +class StokesletWrapper(StokesletWrapperMixin): """ Wrapper class for the Stokeslet kernel. This class is meant to shield the user from the messiness of writing @@ -242,7 +242,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, return sym_expr -class StressletWrapper(StokesletWrapperBase): +class StressletWrapper(StokesletWrapperMixin): """ Wrapper class for the Stresslet kernel. This class is meant to shield the user from the messiness of writing From 44d29bebc0f974b0d7c042a752d2b3880baff6b4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 6 Dec 2020 14:07:20 -0600 Subject: [PATCH 022/209] make flake8 happy --- pytential/symbolic/pde/system_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index ccb1cb282..f63be1f7c 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -90,7 +90,8 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): expr = kernel.get_expression(sym_vec) vec = [] for i in range(len(mis)): - vec.append(_n(expr.xreplace(dict((k, v) for k, v in zip(sym_vec, rand[:, i]))))) + vec.append(_n(expr.xreplace(dict((k, v) for + k, v in zip(sym_vec, rand[:, i]))))) vec = sym.Matrix(vec) result = [] const = 0 From 2db10c681a18a3f8c363d35e6f2dc020f2103368 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 7 Dec 2020 12:26:14 -0600 Subject: [PATCH 023/209] update creating IntGs in stokes --- pytential/symbolic/stokes.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 78b2934cd..e8ef4b420 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -35,31 +35,23 @@ def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, deriv_dirs): """ - Returns the Integral of the Stokeslet/Stresslet kernel given by `icomp` - and `jcomp` and its derivatives. - For instance, - - Stokeslet(icomp, jcomp) - = -1/mu_sym d/(d x_icomp) d/(d x_jcomp) BiharmonicKernel - - for icomp != jcomp. + Returns the Integral of the Stokeslet/Stresslet kernel given by `idx` + and its derivatives. If `use_biharmonic` is set, Biharmonic Kernel + and its derivatives will be used instead of Stokeslet/Stresslet """ - if not self.use_biharmonic: - knl = self.kernel_dict[idx] - for deriv_dir in deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - return sym.IntG(knl, density, - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - - def func(knl, deriv_dirs): + def create_int_g(knl, deriv_dirs, **kwargs): for deriv_dir in deriv_dirs: knl = AxisTargetDerivative(deriv_dir, knl) - res = sym.IntG(knl, density, - qbx_forced_limit=qbx_forced_limit) + res = sym.S(knl, density, + qbx_forced_limit=qbx_forced_limit, **kwargs) return res + if not self.use_biharmonic: + knl = self.kernel_dict[idx] + return create_int_g(knl, deriv_dirs, mu=mu_sym) + deriv_relation = self.deriv_relation_dict[idx] from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE const = deriv_relation[0] @@ -71,7 +63,7 @@ def func(knl, deriv_dirs): new_deriv_dirs = list(deriv_dirs) for idx, val in enumerate(mi): new_deriv_dirs.extend([idx]*val) - result += func(self.base_kernel, new_deriv_dirs) * coeff + result += create_int_g(self.base_kernel, new_deriv_dirs) * coeff return result From d1d8bb02c02dde43cfd81ff492f61cf3c233353e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 7 Dec 2020 12:26:44 -0600 Subject: [PATCH 024/209] pass through timing_data in BoundExpression.__call__ --- pytential/symbolic/execution.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/execution.py b/pytential/symbolic/execution.py index 2f35965e7..2a576edbd 100644 --- a/pytential/symbolic/execution.py +++ b/pytential/symbolic/execution.py @@ -1058,7 +1058,9 @@ def __call__(self, *args, **kwargs): raise TypeError("More than one positional argument supplied. " "None or an ArrayContext expected.") - return self.eval(kwargs, array_context=array_context) + timing_data = kwargs.pop('timing_data', None) + return self.eval(kwargs, array_context=array_context, + timing_data=timing_data) def bind(places, expr, auto_where=None): From 6f458826cc484f9dd97e8f01fc5364375620d562 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 9 Dec 2020 00:17:46 -0600 Subject: [PATCH 025/209] print timing --- test/test_layer_pot.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/test/test_layer_pot.py b/test/test_layer_pot.py index 9a6f547b0..ae5e0712c 100644 --- a/test/test_layer_pot.py +++ b/test/test_layer_pot.py @@ -152,14 +152,14 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): logging.basicConfig(level=logging.INFO) cl_ctx = ctx_factory() - queue = cl.CommandQueue(cl_ctx) + queue = cl.CommandQueue(cl_ctx, properties=cl.command_queue_properties.PROFILING_ENABLE) actx = PyOpenCLArrayContext(queue) # prevent cache 'splosion from sympy.core.cache import clear_cache clear_cache() - nelements = 300 + nelements = 30000*3 target_order = 8 qbx_order = 3 @@ -177,11 +177,13 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): direct_qbx = QBXLayerPotentialSource( pre_density_discr, 4*target_order, qbx_order, fmm_order=False, + fmm_backend="sumpy", target_association_tolerance=0.05, ) fmm_qbx = QBXLayerPotentialSource( pre_density_discr, 4*target_order, qbx_order, fmm_order=qbx_order + 3, + fmm_backend="sumpy", _expansions_in_tree_have_extent=True, target_association_tolerance=0.05, ) @@ -206,9 +208,9 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): op = op_d + op_s * 0.5 try: direct_sigma = direct_density_discr.zeros(actx) + 1 - direct_fld_in_vol = bind(places, op, - auto_where=("direct_qbx", "target"))( - actx, sigma=direct_sigma) + direct_fld_in_vol = 0 #bind(places, op, + # auto_where=("direct_qbx", "target"))( + # actx, sigma=direct_sigma) except QBXTargetAssociationFailedException as e: fplot.show_scalar_in_matplotlib( actx.to_numpy(actx.thaw(e.failed_target_flags))) @@ -218,10 +220,21 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): fmm_sigma = fmm_density_discr.zeros(actx) + 1 fmm_bound_op = bind(places, op, auto_where=("fmm_qbx", "target")) - print(fmm_bound_op.code) - fmm_fld_in_vol = fmm_bound_op(actx, sigma=fmm_sigma) - + #print(fmm_bound_op.code) + fmm_timing_data = {} + fmm_fld_in_vol = fmm_bound_op.eval({"sigma": fmm_sigma}, array_context=actx, timing_data=fmm_timing_data) err = actx.np.fabs(fmm_fld_in_vol - direct_fld_in_vol) + + def print_timing_data(timing_data): + timings = list(fmm_timing_data.values()) + result = {k: 0 for k in timings[0].keys()} + for timing in timings: + for k, v in timing.items(): + result[k] += v['wall_elapsed'] + print(result) + + print_timing_data(fmm_timing_data) + linf_err = actx.to_numpy(err).max() print("l_inf error:", linf_err) @@ -232,16 +245,18 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): ("direct_fld_in_vol", actx.to_numpy(direct_fld_in_vol)) ]) - assert linf_err < 1e-3 + #assert linf_err < 1e-3 # check that using one FMM works op = op_d.copy(source_kernels=op_d.source_kernels + (knl,), densities=op_d.densities + (sym.var("sigma")*0.5,)) single_fmm_bound_op = bind(places, op, auto_where=("fmm_qbx", "target")) - print(single_fmm_bound_op.code) - single_fmm_fld_in_vol = fmm_bound_op(actx, sigma=fmm_sigma) + fmm_timing_data = {} + single_fmm_fld_in_vol = single_fmm_bound_op.eval({"sigma": fmm_sigma}, array_context=actx, timing_data=fmm_timing_data) + err = actx.np.fabs(single_fmm_fld_in_vol - direct_fld_in_vol) + + print_timing_data(fmm_timing_data) - err = actx.np.fabs(fmm_fld_in_vol - single_fmm_fld_in_vol) linf_err = actx.to_numpy(err).max() print("l_inf error:", linf_err) From 995c7b1dd27318bc455710f430db7ade2656c317 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 7 Jan 2021 01:00:47 -0600 Subject: [PATCH 026/209] fix for using new P2M --- pytential/qbx/interactions.py | 19 +++++++++---------- pytential/symbolic/stokes.py | 16 +++++++++++++--- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/pytential/qbx/interactions.py b/pytential/qbx/interactions.py index 63623c8ce..47841ff30 100644 --- a/pytential/qbx/interactions.py +++ b/pytential/qbx/interactions.py @@ -42,8 +42,6 @@ def get_cache_key(self): def get_kernel(self): ncoeffs = len(self.expansion) - print(self.source_kernels) - print(self.expansion) from sumpy.tools import gather_loopy_source_arguments arguments = ( @@ -97,14 +95,14 @@ def get_kernel(self): for isrc <> a[idim] = center[idim] - sources[idim, isrc] \ {dup=idim} - """] + self.get_loopy_instructions() + [""" + """] + [f"<> strength_{i} = strengths[{i}, isrc]" for + i in set(self.strength_usage)] + self.get_loopy_instructions() + [""" end end """] + [f""" qbx_expansions[tgt_icenter, {i}] = \ - simul_reduce(sum, (isrc_box, isrc), \ - {self.get_result_expr(i)}) \ + simul_reduce(sum, (isrc_box, isrc), coeff{i}) \ {{id_prefix=write_expn}} """ for i in range(ncoeffs)] + [""" @@ -220,8 +218,8 @@ def get_kernel(self): fixed_parameters=dict(dim=self.dim), lang_version=MOST_RECENT_LANGUAGE_VERSION) - for expn in [self.src_expansion, self.tgt_expansion]: - loopy_knl = expn.prepare_loopy_kernel(loopy_knl) + for knl in [self.src_expansion.kernel, self.tgt_expansion.kernel]: + loopy_knl = knl.prepare_loopy_kernel(loopy_knl) loopy_knl = lp.tag_inames(loopy_knl, "idim*:unr") @@ -322,8 +320,8 @@ def get_kernel(self): fixed_parameters=dict(dim=self.dim, nchildren=2**self.dim), lang_version=MOST_RECENT_LANGUAGE_VERSION) - for expn in [self.src_expansion, self.tgt_expansion]: - loopy_knl = expn.prepare_loopy_kernel(loopy_knl) + for knl in [self.src_expansion.kernel, self.tgt_expansion.kernel]: + loopy_knl = knl.prepare_loopy_kernel(loopy_knl) loopy_knl = lp.tag_inames(loopy_knl, "idim*:unr") @@ -421,7 +419,8 @@ def get_kernel(self): lang_version=MOST_RECENT_LANGUAGE_VERSION) loopy_knl = lp.tag_inames(loopy_knl, "idim*:unr") - loopy_knl = self.expansion.prepare_loopy_kernel(loopy_knl) + for knl in self.kernels: + loopy_knl = knl.prepare_loopy_kernel(loopy_knl) return loopy_knl diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index e8ef4b420..10d20bfe3 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -24,7 +24,7 @@ from pytential import sym from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, - AxisTargetDerivative, BiharmonicKernel) + AxisTargetDerivative, AxisSourceDerivative, BiharmonicKernel) class StokesletWrapperMixin: @@ -42,11 +42,21 @@ def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, def create_int_g(knl, deriv_dirs, **kwargs): for deriv_dir in deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) + knl = AxisSourceDerivative(deriv_dir, knl) res = sym.S(knl, density, qbx_forced_limit=qbx_forced_limit, **kwargs) - return res + return res*(-1)**len(deriv_dirs) + + def merge_int_gs(*int_gs): + int_gs = list(int_gs) + result = int_gs[0] + for int_g in int_gs[1:]: + result = result.copy( + densities=result.densities + int_g.densities, + source_kernels=result.source_kernels + int_g.source_kernels + ) + return result if not self.use_biharmonic: knl = self.kernel_dict[idx] From bff0a7c0b761b27f274282794c7ca8e202bc0f10 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 20 Jan 2021 13:55:09 -0600 Subject: [PATCH 027/209] Use source derivative and add a function to merge IntGs --- pytential/symbolic/pde/system_utils.py | 72 ++++++++++++++++++++++++++ pytential/symbolic/stokes.py | 16 ++++-- 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index f63be1f7c..b5f0f8de6 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -27,6 +27,10 @@ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) +from pymbolic.mapper import CombineMapper +from pymbolic.primitives import Sum, Product +from pytential.symbolic.primitives import IntG + def _chop(expr, tol): nums = expr.atoms(sym.Number) @@ -116,6 +120,74 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): return res +class HaveIntGs(CombineMapper): + def combine(self, values): + return sum(values) + + def map_int_g(self, expr): + return 1 + + def map_constant(self, expr): + return 0 + + def map_variable(self, expr): + return 0 + + def handle_unsupported_expression(self, expr, *args, **kwargs): + return 0 + + +def process(expr): + have_int_g = HaveIntGs() + if have_int_g(expr) <= 1: + return expr + return sum(_process(expr)) + + +def _process(expr): + if isinstance(expr, Sum): + result_coeff = 0 + result_int_g = 0 + for c in expr.children: + coeff, int_g = _process(c) + result_coeff += coeff + if int_g == 0: + continue + if result_int_g == 0: + result_int_g = int_g + continue + assert result_int_g.source == int_g.source + assert result_int_g.target == int_g.target + assert result_int_g.qbx_forced_limit == int_g.qbx_forced_limit + assert result_int_g.kernel_arguments == int_g.kernel_arguments + assert result_int_g.target_kernel == int_g.target_kernel + result_int_g = result_int_g.copy( + source_kernels=(result_int_g.source_kernels + int_g.source_kernels), + densities=(result_int_g.densities + int_g.densities) + ) + return result_coeff, result_int_g + elif isinstance(expr, Product): + mult = 1 + found_int_g = None + have_int_g = HaveIntGs() + for c in expr.children: + if not have_int_g(c): + mult *= c + elif found_int_g: + raise RuntimeError("Not a linear expression.") + else: + found_int_g = c + if not found_int_g: + return expr, 0 + else: + coeff, new_int_g = _process(found_int_g) + new_densities = (density * mult for density in new_int_g.densities) + return coeff*mult, new_int_g.copy(densities=new_densities) + elif isinstance(expr, IntG): + return 0, expr + else: + return expr, 0 + if __name__ == "__main__": from sumpy.kernel import StokesletKernel, BiharmonicKernel, StressletKernel base_kernel = BiharmonicKernel(2) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 10d20bfe3..a8292c442 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -42,11 +42,17 @@ def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, def create_int_g(knl, deriv_dirs, **kwargs): for deriv_dir in deriv_dirs: - knl = AxisSourceDerivative(deriv_dir, knl) + if self.use_source: + knl = AxisSourceDerivative(deriv_dir, knl) + else: + knl = AxisTargetDerivative(deriv_dir, knl) res = sym.S(knl, density, qbx_forced_limit=qbx_forced_limit, **kwargs) - return res*(-1)**len(deriv_dirs) + if self.use_source: + return res*(-1)**len(deriv_dirs) + else: + return res def merge_int_gs(*int_gs): int_gs = list(int_gs) @@ -107,7 +113,7 @@ class StokesletWrapper(StokesletWrapperMixin): for the same kernel in a different ordering. """ - def __init__(self, dim=None, use_biharmonic=True): + def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.use_biharmonic = use_biharmonic self.dim = dim if not (dim == 3 or dim == 2): @@ -116,6 +122,7 @@ def __init__(self, dim=None, use_biharmonic=True): self.kernel_dict = {} self.base_kernel = BiharmonicKernel(dim=dim) + self.use_source = use_source for i in range(dim): for j in range(i, dim): @@ -273,7 +280,7 @@ class StressletWrapper(StokesletWrapperMixin): """ - def __init__(self, dim=None, use_biharmonic=True): + def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.use_biharmonic = use_biharmonic self.dim = dim if not (dim == 3 or dim == 2): @@ -282,6 +289,7 @@ def __init__(self, dim=None, use_biharmonic=True): self.kernel_dict = {} self.base_kernel = BiharmonicKernel(dim=dim) + self.use_source = use_source for i in range(dim): for j in range(i, dim): From 8ed8214596c25196ea60e9173199a8778ee67308 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 20 Jan 2021 13:55:46 -0600 Subject: [PATCH 028/209] biharmonic stokes test --- test/test_stokes.py | 54 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index e7635e966..026382a0c 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -42,9 +42,9 @@ def run_exterior_stokes_2d(ctx_factory, nelements, - mesh_order=4, target_order=4, qbx_order=4, - fmm_order=10, - mu=1, circle_rad=1.5, visualize=False): + target_order=3, qbx_order=7, + fmm_order=13, + mu=1, circle_rad=1.5, visualize=False, use_biharmonic=True): # This program tests an exterior Stokes flow in 2D using the # compound representation given in Hsiao & Kress, @@ -57,7 +57,7 @@ def run_exterior_stokes_2d(ctx_factory, nelements, queue = cl.CommandQueue(cl_ctx) actx = PyOpenCLArrayContext(queue) - ovsmp_target_order = 4*target_order + ovsmp_target_order = 8*target_order # {{{ geometries @@ -133,8 +133,12 @@ def outside_circle(test_points, radius): # +1 for exterior Dirichlet loc_sign = 1 - stresslet_obj = StressletWrapper(dim=2) - stokeslet_obj = StokesletWrapper(dim=2) + stresslet_obj = StressletWrapper(dim=2, use_biharmonic=use_biharmonic) + stokeslet_obj = StokesletWrapper(dim=2, use_biharmonic=use_biharmonic) + #stresslet_obj = StressletWrapper(dim=2, use_biharmonic=False) + #stokeslet_obj = StokesletWrapper(dim=2, use_biharmonic=False) + #stresslet_obj = StressletWrapper(dim=2, use_biharmonic=True, use_source=False) + #stokeslet_obj = StokesletWrapper(dim=2, use_biharmonic=True, use_source=False) bdry_op_sym = ( -loc_sign * 0.5 * sigma_sym - stresslet_obj.apply(sigma_sym, nvec_sym, mu_sym, @@ -142,9 +146,14 @@ def outside_circle(test_points, radius): + stokeslet_obj.apply(meanless_sigma_sym, mu_sym, qbx_forced_limit="avg") - (0.5/np.pi) * int_sigma) + from pytential.symbolic.pde.system_utils import process + if use_biharmonic: + bdry_op_sym = np.array([process(expr) for expr in bdry_op_sym]) + # }}} bound_op = bind(places, bdry_op_sym) + print(bound_op.code) # {{{ fix rhs and solve @@ -192,8 +201,16 @@ def fund_and_rot_soln(x, y, loc, strength): ])) bvp_rhs = bind(places, - sym.make_sym_vector("bc", dim) + u_A_sym_bdry)(actx, - bc=bc, mu=mu, omega=omega) + sym.make_sym_vector("bc", dim) + u_A_sym_bdry)(actx, bc=bc, mu=mu, omega=omega) + from time import time + scipy_op = bound_op.scipy_op(actx, "sigma", np.float64, mu=mu, normal=normal) + res = scipy_op.matvec(bvp_rhs) + print("start") + start = time() + res = scipy_op.matvec(bvp_rhs) + print(time()-start) + 1/0 + gmres_result = gmres( bound_op.scipy_op(actx, "sigma", np.float64, mu=mu, normal=normal), bvp_rhs, @@ -212,6 +229,8 @@ def fund_and_rot_soln(x, y, loc, strength): int_val = -int_val/(2 * np.pi) print("int_val = ", int_val) + #stresslet_obj = StressletWrapper(dim=2, use_biharmonic=True, use_source=False) + #stokeslet_obj = StokesletWrapper(dim=2, use_biharmonic=True, use_source=False) u_A_sym_vol = stokeslet_obj.apply( # noqa: N806 omega_sym, mu_sym, qbx_forced_limit=2) representation_sym = ( @@ -300,16 +319,25 @@ def get_obj_array(obj_array): return h_max, l2_err -def test_exterior_stokes_2d(ctx_factory, qbx_order=3): +def test_exterior_stokes_2d(ctx_factory, qbx_order=3, use_biharmonic=True): from pytools.convergence import EOCRecorder eoc_rec = EOCRecorder() - - for nelements in [20, 50]: - h_max, l2_err = run_exterior_stokes_2d(ctx_factory, nelements) + target_order = 3 + fmm_order = 10 + + for nelements in [100000]: + h_max, l2_err = run_exterior_stokes_2d( + ctx_factory, nelements, + qbx_order=qbx_order, + target_order=target_order, + fmm_order=fmm_order, + use_biharmonic=use_biharmonic + ) eoc_rec.add_data_point(h_max, l2_err) + print(eoc_rec) print(eoc_rec) - assert eoc_rec.order_estimate() >= qbx_order - 1 + assert eoc_rec.order_estimate() >= target_order - 0.5 # You can test individual routines by typing From f7c3ace1ef1972eb03c8fddb5723e579e40e6887 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 20 Jan 2021 18:58:24 -0600 Subject: [PATCH 029/209] working biharmonic now --- pytential/symbolic/pde/system_utils.py | 18 +++++++--- pytential/symbolic/stokes.py | 47 +++++++++++++------------- test/test_stokes.py | 18 +++++----- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index b5f0f8de6..279302e97 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -137,19 +137,27 @@ def handle_unsupported_expression(self, expr, *args, **kwargs): return 0 -def process(expr): +def merge_int_g_exprs(exprs): + return np.array([merge_int_g_expr(expr) for expr in exprs]) + + +def merge_int_g_expr(expr): have_int_g = HaveIntGs() if have_int_g(expr) <= 1: return expr - return sum(_process(expr)) + try: + res = sum(_merge_int_g_expr(expr)) + return res + except AssertionError: + return expr -def _process(expr): +def _merge_int_g_expr(expr): if isinstance(expr, Sum): result_coeff = 0 result_int_g = 0 for c in expr.children: - coeff, int_g = _process(c) + coeff, int_g = _merge_int_g_expr(c) result_coeff += coeff if int_g == 0: continue @@ -180,7 +188,7 @@ def _process(expr): if not found_int_g: return expr, 0 else: - coeff, new_int_g = _process(found_int_g) + coeff, new_int_g = _merge_int_g_expr(found_int_g) new_densities = (density * mult for density in new_int_g.densities) return coeff*mult, new_int_g.copy(densities=new_densities) elif isinstance(expr, IntG): diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index cc8be08d9..8767da7b2 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -23,6 +23,7 @@ import numpy as np from pytential import sym +from pytential.symbolic.pde.system_utils import merge_int_g_exprs from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, AxisTargetDerivative, AxisSourceDerivative, BiharmonicKernel) @@ -71,7 +72,12 @@ def create_int_g(knl, deriv_dirs, **kwargs): deriv_relation = self.deriv_relation_dict[idx] const = deriv_relation[0] - const *= sym.integral(self.dim, self.dim-1, density, dofdesc=self.source_dofdesc) + + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) + const *= sym.integral(self.dim, self.dim-1, density, dofdesc=dd) result = const for mi, coeff in deriv_relation[1]: @@ -118,7 +124,7 @@ class StokesletWrapper(StokesletWrapperMixin): .. automethod:: apply_stress """ - def __init__(self, dim=None, use_biharmonic=True, use_source=True, source_dofdesc=None): + def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.use_biharmonic = use_biharmonic self.dim = dim if not (dim == 3 or dim == 2): @@ -128,7 +134,6 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True, source_dofdes self.base_kernel = BiharmonicKernel(dim=dim) self.use_source = use_source - self.source_dofdesc = source_dofdesc for i in range(dim): for j in range(i, dim): @@ -287,7 +292,7 @@ class StressletWrapper(StokesletWrapperMixin): .. automethod:: apply_stress """ - def __init__(self, dim=None, use_biharmonic=True, use_source=True, source_dofdesc=None): + def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.use_biharmonic = use_biharmonic self.dim = dim if not (dim == 3 or dim == 2): @@ -297,7 +302,6 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True, source_dofdes self.base_kernel = BiharmonicKernel(dim=dim) self.use_source = use_source - self.source_dofdesc = source_dofdesc for i in range(dim): for j in range(i, dim): @@ -472,16 +476,8 @@ def __init__(self, ambient_dim, side, use_biharmonic): self.ambient_dim = ambient_dim self.side = side - # NOTE: we set a dofdesc here to force the evaluation of this integral - # on the source instead of the target when using automatic tagging - # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` - self.source_dofdesc = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) - from pytential.symbolic.primitives import as_dofdesc, DEFAULT_SOURCE - self.source_dofdesc = as_dofdesc(DEFAULT_SOURCE) - self.source_dofdesc = "source" - - self.stresslet = StressletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic, source_dofdesc=self.source_dofdesc) - self.stokeslet = StokesletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic, source_dofdesc=self.source_dofdesc) + self.stresslet = StressletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic) + self.stokeslet = StokesletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic) self.use_biharmonic = use_biharmonic @@ -566,7 +562,8 @@ def __init__(self, *, omega, alpha=None, eta=None, use_biharmonic=False): self.eta = eta def _farfield(self, mu, qbx_forced_limit): - length = sym.integral(self.ambient_dim, self.dim, 1) + source_dofdesc = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) + length = sym.integral(self.ambient_dim, self.dim, 1, dofdesc=source_dofdesc) return self.stokeslet.apply( -self.omega / length, mu, @@ -575,11 +572,15 @@ def _farfield(self, mu, qbx_forced_limit): def _operator(self, sigma, normal, mu, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit if slp_qbx_forced_limit == "avg": - slp_qbx_forced_limit = +1 + slp_qbx_forced_limit = "avg" - int_sigma = sym.integral(self.ambient_dim, self.dim, sigma, dofdesc=self.source_dofdesc) + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) + int_sigma = sym.integral(self.ambient_dim, self.dim, sigma, dofdesc=dd) - meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, self.dim, sigma, dofdesc=self.source_dofdesc)) + meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, self.dim, sigma, dofdesc=dd)) op_k = self.stresslet.apply(sigma, normal, mu, qbx_forced_limit=qbx_forced_limit) @@ -596,11 +597,11 @@ def prepare_rhs(self, b, *, mu): def operator(self, sigma, *, normal, mu): # NOTE: H. K. 1985 Equation 2.18 - return -0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg") + return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg")) def velocity(self, sigma, *, normal, mu, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 - return ( + return merge_int_g_exprs( -self._farfield(mu, qbx_forced_limit) - self._operator(sigma, normal, mu, qbx_forced_limit) ) @@ -652,11 +653,11 @@ def _operator(self, sigma, normal, mu, qbx_forced_limit): def operator(self, sigma, *, normal, mu): # NOTE: H. 1986 Equation 17 - return -0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg") + return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg")) def velocity(self, sigma, *, normal, mu, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 - return -self._operator(sigma, normal, mu, qbx_forced_limit) + return merge_int_g_exprs(-self._operator(sigma, normal, mu, qbx_forced_limit)) def pressure(self, sigma, *, normal, mu, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the diff --git a/test/test_stokes.py b/test/test_stokes.py index 6aef9b6b0..71bb76895 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -150,7 +150,8 @@ def run_exterior_stokes(ctx_factory, *, sym_velocity = op.velocity(sym_sigma, normal=sym_normal, mu=sym_mu) - sym_source_pot = op.stokeslet.apply(sym_sigma, sym_mu, qbx_forced_limit=None) + from pytential.symbolic.stokes import StokesletWrapper + sym_source_pot = StokesletWrapper(ambient_dim, use_biharmonic=False).apply(sym_sigma, sym_mu, qbx_forced_limit=None) # }}} @@ -177,17 +178,13 @@ def run_exterior_stokes(ctx_factory, *, bc_context = {} op_context = {"mu": mu, "normal": normal} - from meshmode.dof_array import unflatten, flatten, thaw - bc = bind(places, sym_source_pot, - auto_where=("point_source", "source"))(actx, sigma=charges, mu=mu) + bc_op = bind(places, sym_source_pot, + auto_where=("point_source", "source")) + bc = bc_op(actx, sigma=charges, mu=mu) rhs = bind(places, sym_rhs)(actx, bc=bc, **bc_context) bound_op = bind(places, sym_op) - from pytential.symbolic.pde.system_utils import process - if False: - bdry_op_sym = np.array([process(expr) for expr in bdry_op_sym]) - # }}} from time import time @@ -197,7 +194,6 @@ def run_exterior_stokes(ctx_factory, *, start = time() res = scipy_op.matvec(rhs) print(time()-start) - 1/0 # {{{ solve @@ -275,10 +271,11 @@ def test_exterior_stokes(ctx_factory, ambient_dim, visualize=False, use_biharmon target_order = 3 qbx_order = 3 - print(ambient_dim) if ambient_dim == 2: + fmm_order = 10 resolutions = [20, 35, 50] elif ambient_dim == 3: + fmm_order = 8 resolutions = [0, 1, 2] else: raise ValueError(f"unsupported dimension: {ambient_dim}") @@ -287,6 +284,7 @@ def test_exterior_stokes(ctx_factory, ambient_dim, visualize=False, use_biharmon h_max, err = run_exterior_stokes(ctx_factory, ambient_dim=ambient_dim, target_order=target_order, + fmm_order=fmm_order, qbx_order=qbx_order, resolution=resolution, visualize=visualize, From 6898f2442601f6d10f56b41b5a050d31c6851d31 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 29 Apr 2021 11:37:58 -0500 Subject: [PATCH 030/209] Refactor StokesletWrapper --- pytential/symbolic/stokes.py | 351 ++++++++++++++++++++--------------- 1 file changed, 204 insertions(+), 147 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 8767da7b2..05caca752 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -39,9 +39,187 @@ # {{{ StokesletWrapper +class StokesletWrapperBase: + """Wrapper class for the :class:`~sumpy.kernel.StokesletKernel` kernel. + + This class is meant to shield the user from the messiness of writing + out every term in the expansion of the double-indexed Stokeslet kernel + applied to the density vector. The object is created + to do some of the set-up and bookkeeping once, rather than every + time we want to create a symbolic expression based on the kernel -- say, + once when we solve for the density, and once when we want a symbolic + representation for the solution, for example. + + The :meth:`apply` function returns the integral expressions needed for + the vector velocity resulting from convolution with the vector density, + and is meant to work similarly to calling + :func:`~pytential.symbolic.primitives.S` (which is + :class:`~pytential.symbolic.primitives.IntG`). + + Similar functions are available for other useful things related to + the flow: :meth:`apply_pressure`, :meth:`apply_derivative` (target derivative), + :meth:`apply_stress` (applies symmetric viscous stress tensor in + the requested direction). + + .. automethod:: apply + .. automethod:: apply_pressure + .. automethod:: apply_derivative + .. automethod:: apply_stress + """ + + def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): + """Symbolic expressions for integrating Stokeslet kernel. + + Returns an object array of symbolic expressions for the vector + resulting from integrating the dyadic Stokeslet kernel with + variable *density_vec_sym*. + + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg mu_sym: a symbolic variable for the viscosity. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + raise NotImplementedError + + def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): + """Symbolic expression for pressure field associated with the Stokeslet.""" + raise NotImplementedError + + def apply_derivative(self, deriv_dir, density_vec_sym, + mu_sym, qbx_forced_limit): + """Symbolic derivative of velocity from Stokeslet. + + Returns an object array of symbolic expressions for the vector + resulting from integrating the *deriv_dir* target derivative of the + dyadic Stokeslet kernel with variable *density_vec_sym*. + + :arg deriv_dir: integer denoting the axis direction for the derivative. + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg mu_sym: a symbolic variable for the viscosity. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + raise NotImplementedError + + def apply_stress(self, density_vec_sym, dir_vec_sym, + mu_sym, qbx_forced_limit): + r"""Symbolic expression for viscous stress applied to a direction. + + Returns a vector of symbolic expressions for the force resulting + from the viscous stress + + .. math:: + + -p \delta_{ij} + \mu (\nabla_i u_j + \nabla_j u_i) + + applied in the direction of *dir_vec_sym*. + + Note that this computation is very similar to computing + a double-layer potential with the Stresslet kernel in + :class:`StressletWrapper`. The difference is that here the direction + vector is applied at the target points, while in the Stresslet the + direction is applied at the source points. + + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg dir_vec_sym: a symbolic vector for the application direction. + :arg mu_sym: a symbolic variable for the viscosity. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + raise NotImplementedError + + +class StressletWrapperBase: + """Wrapper class for the :class:`~sumpy.kernel.StressletKernel` kernel. + + This class is meant to shield the user from the messiness of writing + out every term in the expansion of the triple-indexed Stresslet + kernel applied to both a normal vector and the density vector. + The object is created to do some of the set-up and bookkeeping once, + rather than every time we want to create a symbolic expression based + on the kernel -- say, once when we solve for the density, and once when + we want a symbolic representation for the solution, for example. + + The :meth:`apply` function returns the integral expressions needed for + convolving the kernel with a vector density, and is meant to work + similarly to :func:`~pytential.symbolic.primitives.S` (which is + :class:`~pytential.symbolic.primitives.IntG`). + + Similar functions are available for other useful things related to + the flow: :meth:`apply_pressure`, :meth:`apply_derivative` (target derivative), + :meth:`apply_stress` (applies symmetric viscous stress tensor in + the requested direction). + + .. automethod:: apply + .. automethod:: apply_pressure + .. automethod:: apply_derivative + .. automethod:: apply_stress + """ + + def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + """Symbolic expressions for integrating Stresslet kernel. + + Returns an object array of symbolic expressions for the vector + resulting from integrating the dyadic Stresslet kernel with + variable *density_vec_sym* and source direction vectors *dir_vec_sym*. + + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg dir_vec_sym: a symbolic vector variable for the direction vector. + :arg mu_sym: a symbolic variable for the viscosity. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + raise NotImplementedError + + def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + """Symbolic expression for pressure field associated with the Stresslet.""" + raise NotImplementedError + + def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, + mu_sym, qbx_forced_limit): + """Symbolic derivative of velocity from stresslet. + + Returns an object array of symbolic expressions for the vector + resulting from integrating the *deriv_dir* target derivative of the + dyadic Stresslet kernel with variable *density_vec_sym* and source + direction vectors *dir_vec_sym*. + + :arg deriv_dir: integer denoting the axis direction for the derivative. + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg dir_vec_sym: a symbolic vector variable for the normal direction. + :arg mu_sym: a symbolic variable for the viscosity. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + raise NotImplementedError + + def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, + mu_sym, qbx_forced_limit): + r"""Symbolic expression for viscous stress applied to a direction. + + Returns a vector of symbolic expressions for the force resulting + from the viscous stress + + .. math:: + + -p \delta_{ij} + \mu (\nabla_i u_j + \nabla_j u_i) + + applied in the direction of *dir_vec_sym*. + + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg normal_vec_sym: a symbolic vector variable for the normal vectors + (outward facing normals at source locations). + :arg dir_vec_sym: a symbolic vector for the application direction. + :arg mu_sym: a symbolic variable for the viscosity. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + raise NotImplementedError + + class StokesletWrapperMixin: """A base class for StokesletWrapper and StressletWrapper - + to create IntG instances """ def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, deriv_dirs): @@ -89,47 +267,14 @@ def create_int_g(knl, deriv_dirs, **kwargs): return result -class StokesletWrapper(StokesletWrapperMixin): - """Wrapper class for the :class:`~sumpy.kernel.StokesletKernel` kernel. - - This class is meant to shield the user from the messiness of writing - out every term in the expansion of the double-indexed Stokeslet kernel - applied to the density vector. The object is created - to do some of the set-up and bookkeeping once, rather than every - time we want to create a symbolic expression based on the kernel -- say, - once when we solve for the density, and once when we want a symbolic - representation for the solution, for example. - - The :meth:`apply` function returns the integral expressions needed for - the vector velocity resulting from convolution with the vector density, - and is meant to work similarly to calling - :func:`~pytential.symbolic.primitives.S` (which is - :class:`~pytential.symbolic.primitives.IntG`). - - Similar functions are available for other useful things related to - the flow: :meth:`apply_pressure`, :meth:`apply_derivative` (target derivative), - :meth:`apply_stress` (applies symmetric viscous stress tensor in - the requested direction). - - .. attribute:: kernel_dict - - The dictionary allows us to exploit symmetry -- that - :math:`S_{01}` is identical to :math:`S_{10}` -- and avoid creating - multiple expansions for the same kernel in a different ordering. - - .. automethod:: __init__ - .. automethod:: apply - .. automethod:: apply_pressure - .. automethod:: apply_derivative - .. automethod:: apply_stress - """ - +class StokesletWrapper(StokesletWrapperBase, StokesletWrapperMixin): def __init__(self, dim=None, use_biharmonic=True, use_source=True): - self.use_biharmonic = use_biharmonic self.dim = dim if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") + self.use_biharmonic = use_biharmonic + self.kernel_dict = {} self.base_kernel = BiharmonicKernel(dim=dim) @@ -140,6 +285,9 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, jcomp=j) + # The dictionary allows us to exploit symmetry -- that + # :math:`T_{01}` is identical to :math:`T_{10}` -- and avoid creating + # multiple expansions for the same kernel in a different ordering. for i in range(dim): for j in range(i): self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] @@ -153,17 +301,6 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.deriv_relation_dict[idx] = deriv_eq def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): - """Symbolic expressions for integrating Stokeslet kernel. - - Returns an object array of symbolic expressions for the vector - resulting from integrating the dyadic Stokeslet kernel with - variable *density_vec_sym*. - - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg mu_sym: a symbolic variable for the viscosity. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ sym_expr = np.zeros((self.dim,), dtype=object) @@ -175,7 +312,6 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): return sym_expr def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): - """Symbolic expression for pressure field associated with the Stokeslet.""" from pytential.symbolic.mappers import DerivativeTaker kernel = LaplaceKernel(dim=self.dim) @@ -190,18 +326,6 @@ def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): def apply_derivative(self, deriv_dir, density_vec_sym, mu_sym, qbx_forced_limit): - """Symbolic derivative of velocity from Stokeslet. - - Returns an object array of symbolic expressions for the vector - resulting from integrating the *deriv_dir* target derivative of the - dyadic Stokeslet kernel with variable *density_vec_sym*. - - :arg deriv_dir: integer denoting the axis direction for the derivative. - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg mu_sym: a symbolic variable for the viscosity. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ sym_expr = self.apply(density_vec_sym, mu_sym, qbx_forced_limit) @@ -215,29 +339,6 @@ def apply_derivative(self, deriv_dir, density_vec_sym, def apply_stress(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): - r"""Symbolic expression for viscous stress applied to a direction. - - Returns a vector of symbolic expressions for the force resulting - from the viscous stress - - .. math:: - - -p \delta_{ij} + \mu (\nabla_i u_j + \nabla_j u_i) - - applied in the direction of *dir_vec_sym*. - - Note that this computation is very similar to computing - a double-layer potential with the Stresslet kernel in - :class:`StressletWrapper`. The difference is that here the direction - vector is applied at the target points, while in the Stresslet the - direction is applied at the source points. - - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg dir_vec_sym: a symbolic vector for the application direction. - :arg mu_sym: a symbolic variable for the viscosity. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = StressletWrapper(dim=self.dim, @@ -258,7 +359,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, # {{{ StressletWrapper -class StressletWrapper(StokesletWrapperMixin): +class StressletWrapper(StressletWrapperBase, StokesletWrapperMixin): """Wrapper class for the :class:`~sumpy.kernel.StressletKernel` kernel. This class is meant to shield the user from the messiness of writing @@ -279,12 +380,6 @@ class StressletWrapper(StokesletWrapperMixin): :meth:`apply_stress` (applies symmetric viscous stress tensor in the requested direction). - .. attribute:: kernel_dict - - The dictionary allows us to exploit symmetry -- that - :math:`T_{012}` is identical to :math:`T_{120}` -- and avoid creating - multiple expansions for the same kernel in a different ordering. - .. automethod:: __init__ .. automethod:: apply .. automethod:: apply_pressure @@ -309,6 +404,9 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.kernel_dict[(i, j, k)] = StressletKernel(dim=dim, icomp=i, jcomp=j, kcomp=k) + # The dictionary allows us to exploit symmetry -- that + # :math:`T_{012}` is identical to :math:`T_{120}` -- and avoid creating + # multiple expansions for the same kernel in a different ordering. for i in range(dim): for j in range(dim): for k in range(dim): @@ -326,18 +424,6 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.deriv_relation_dict[idx] = deriv_eq def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): - """Symbolic expressions for integrating Stresslet kernel. - - Returns an object array of symbolic expressions for the vector - resulting from integrating the dyadic Stresslet kernel with - variable *density_vec_sym* and source direction vectors *dir_vec_sym*. - - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg dir_vec_sym: a symbolic vector variable for the direction vector. - :arg mu_sym: a symbolic variable for the viscosity. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ sym_expr = np.zeros((self.dim,), dtype=object) @@ -351,7 +437,6 @@ def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): return sym_expr def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): - """Symbolic expression for pressure field associated with the Stresslet.""" import itertools from pytential.symbolic.mappers import DerivativeTaker @@ -372,20 +457,6 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit) def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): - """Symbolic derivative of velocity from stresslet. - - Returns an object array of symbolic expressions for the vector - resulting from integrating the *deriv_dir* target derivative of the - dyadic Stresslet kernel with variable *density_vec_sym* and source - direction vectors *dir_vec_sym*. - - :arg deriv_dir: integer denoting the axis direction for the derivative. - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg dir_vec_sym: a symbolic vector variable for the normal direction. - :arg mu_sym: a symbolic variable for the viscosity. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ sym_expr = np.zeros((self.dim,), dtype=object) @@ -400,25 +471,6 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): - r"""Symbolic expression for viscous stress applied to a direction. - - Returns a vector of symbolic expressions for the force resulting - from the viscous stress - - .. math:: - - -p \delta_{ij} + \mu (\nabla_i u_j + \nabla_j u_i) - - applied in the direction of *dir_vec_sym*. - - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg normal_vec_sym: a symbolic vector variable for the normal vectors - (outward facing normals at source locations). - :arg dir_vec_sym: a symbolic vector for the application direction. - :arg mu_sym: a symbolic variable for the viscosity. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ sym_expr = np.empty((self.dim,), dtype=object) @@ -476,10 +528,11 @@ def __init__(self, ambient_dim, side, use_biharmonic): self.ambient_dim = ambient_dim self.side = side - self.stresslet = StressletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic) - self.stokeslet = StokesletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic) + self.stresslet = StressletWrapper(dim=self.ambient_dim, + use_biharmonic=use_biharmonic) + self.stokeslet = StokesletWrapper(dim=self.ambient_dim, + use_biharmonic=use_biharmonic) self.use_biharmonic = use_biharmonic - @property def dim(self): @@ -579,8 +632,9 @@ def _operator(self, sigma, normal, mu, qbx_forced_limit): # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) int_sigma = sym.integral(self.ambient_dim, self.dim, sigma, dofdesc=dd) - - meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, self.dim, sigma, dofdesc=dd)) + + meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, + self.dim, sigma, dofdesc=dd)) op_k = self.stresslet.apply(sigma, normal, mu, qbx_forced_limit=qbx_forced_limit) @@ -597,7 +651,8 @@ def prepare_rhs(self, b, *, mu): def operator(self, sigma, *, normal, mu): # NOTE: H. K. 1985 Equation 2.18 - return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg")) + return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator( + sigma, normal, mu, "avg")) def velocity(self, sigma, *, normal, mu, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 @@ -641,8 +696,8 @@ def __init__(self, *, eta=None, use_biharmonic=False): def _operator(self, sigma, normal, mu, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit - if slp_qbx_forced_limit == "avg": - slp_qbx_forced_limit = self.side + # if slp_qbx_forced_limit == "avg": + # slp_qbx_forced_limit = self.side op_w = self.stresslet.apply(sigma, normal, mu, qbx_forced_limit=qbx_forced_limit) @@ -653,11 +708,13 @@ def _operator(self, sigma, normal, mu, qbx_forced_limit): def operator(self, sigma, *, normal, mu): # NOTE: H. 1986 Equation 17 - return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg")) + return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, + normal, mu, "avg")) def velocity(self, sigma, *, normal, mu, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 - return merge_int_g_exprs(-self._operator(sigma, normal, mu, qbx_forced_limit)) + return merge_int_g_exprs(-self._operator(sigma, normal, mu, + qbx_forced_limit)) def pressure(self, sigma, *, normal, mu, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the From 2dc482dd2512f5cd95b14852f6d15b0ed99778d2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 29 Apr 2021 15:18:10 -0500 Subject: [PATCH 031/209] use itgt instead of icenter_tgt for TargetPointMultiplier --- pytential/qbx/interactions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytential/qbx/interactions.py b/pytential/qbx/interactions.py index 47841ff30..1b0ce96b1 100644 --- a/pytential/qbx/interactions.py +++ b/pytential/qbx/interactions.py @@ -381,9 +381,9 @@ def get_kernel(self): for icenter_tgt - <> center_itgt = center_to_targets_lists[icenter_tgt] + <> itgt = center_to_targets_lists[icenter_tgt] - <> b[idim] = targets[idim, center_itgt] - center[idim] + <> b[idim] = targets[idim, itgt] - center[idim] """] + [""" <> coeff{i} = qbx_expansions[src_icenter, {i}] @@ -391,7 +391,7 @@ def get_kernel(self): ] + loopy_insns + [""" - result[{i},center_itgt] = kernel_scaling * result_{i}_p \ + result[{i},itgt] = kernel_scaling * result_{i}_p \ {{id_prefix=write_result}} """.format(i=i) for i in range(len(result_names))] + [""" From 31717da530346e95a8c409401cb9d0e164ae5add Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 29 Apr 2021 15:19:27 -0500 Subject: [PATCH 032/209] Use transformation remover --- pytential/source.py | 8 ++++---- pytential/symbolic/primitives.py | 6 +++--- pytential/unregularized.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pytential/source.py b/pytential/source.py index fef002154..1ae2cc645 100644 --- a/pytential/source.py +++ b/pytential/source.py @@ -128,10 +128,10 @@ def ambient_dim(self): return self._nodes.shape[0] def op_group_features(self, expr): - from sumpy.kernel import AxisTargetDerivativeRemover + from sumpy.kernel import TargetTransformationRemover result = ( expr.source, expr.densities, expr.source_kernels, - AxisTargetDerivativeRemover()(expr.target_kernel), + TargetTransformationRemover()(expr.target_kernel), ) return result @@ -254,9 +254,9 @@ def complex_dtype(self): def get_fmm_kernel(self, kernels): fmm_kernel = None - from sumpy.kernel import AxisTargetDerivativeRemover + from sumpy.kernel import TargetTransformationRemover for knl in kernels: - candidate_fmm_kernel = AxisTargetDerivativeRemover()(knl) + candidate_fmm_kernel = TargetTransformationRemover()(knl) if fmm_kernel is None: fmm_kernel = candidate_fmm_kernel diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index d28d9cb1a..4f0e65416 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1658,9 +1658,9 @@ class _unspecified: # noqa: N801 def _create_int_g(kernel, density, qbx_forced_limit, source, target, kernel_arguments, **kwargs): - from sumpy.kernel import SourceDerivativeRemover, TargetDerivativeRemover - sdr = SourceDerivativeRemover() - tdr = TargetDerivativeRemover() + from sumpy.kernel import SourceTransformationRemover, TargetTransformationRemover + sdr = SourceTransformationRemover() + tdr = TargetTransformationRemover() target_kernel = sdr(kernel) source_kernels = [tdr(kernel)] diff --git a/pytential/unregularized.py b/pytential/unregularized.py index c3db628e8..34396f116 100644 --- a/pytential/unregularized.py +++ b/pytential/unregularized.py @@ -118,10 +118,10 @@ def evaluate_wrapper(expr): return func(actx, insn, bound_expr, evaluate_wrapper) def op_group_features(self, expr): - from sumpy.kernel import AxisTargetDerivativeRemover + from sumpy.kernel import TargetTransformationRemover result = ( expr.source, expr.densities, expr.source_kernels, - AxisTargetDerivativeRemover()(expr.target_kernel), + TargetTransformationRemover()(expr.target_kernel), ) return result From 8233ce6514c162242ba50e4cb097f767609460b8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 2 May 2021 13:57:33 -0500 Subject: [PATCH 033/209] Add Tornberg algorithm --- pytential/symbolic/pde/system_utils.py | 100 +++++++++++-- pytential/symbolic/stokes.py | 200 +++++++++++++++++++------ test/test_maxwell.py | 5 +- test/test_stokes.py | 39 ++--- 4 files changed, 268 insertions(+), 76 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 279302e97..ff1b7a304 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -23,6 +23,8 @@ import numpy as np from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper +from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, + DirectionalSourceDerivative) from pytools import ( generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) @@ -100,9 +102,9 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): result = [] const = 0 if verbose: - print(kernel, end=" = ") + print(kernel, end=" = ", flush=True) for i, coeff in enumerate(mat.solve(vec)): - coeff = _chop(coeff, tol) + coeff = _chop(coeff.expand(), tol) if coeff == 0: continue if mis[i] != (-1, -1, -1): @@ -141,17 +143,83 @@ def merge_int_g_exprs(exprs): return np.array([merge_int_g_expr(expr) for expr in exprs]) +def _convert_target_deriv_to_source(int_g): + knl = int_g.target_kernel + source_kernels = list(int_g.source_kernels) + coeff = 1 + while isinstance(knl, AxisTargetDerivative): + coeff *= 1 + source_kernels = [AxisSourceDerivative(knl.axis, source_knl) for + source_knl in source_kernels] + knl = knl.inner_kernel + return coeff * int_g.copy(target_kernel=knl, + source_kernels=tuple(source_kernels)) + + +def _convert_axis_source_to_directional_source(int_g): + if not isinstance(int_g, IntG): + return int_g + knls = list(int_g.source_kernels) + dim = knls[0].dim + if len(knls) != dim: + return int_g + if not any(isinstance(knl, AxisSourceDerivative) for knl in knls): + return int_g + # TODO: sort + axes = [knl.axis for knl in knls] + if axes != list(range(dim)): + return int_g + base_knls = set(knl.inner_kernel for knl in knls) + if len(base_knls) > 1: + return int_g + base_knl = base_knls.pop() + kernel_arguments = int_g.kernel_arguments.copy() + name = "generated_dir_vec" + kernel_arguments[name] = \ + np.array(int_g.densities, dtype=np.object) + res = int_g.copy( + source_kernels=( + DirectionalSourceDerivative(base_knl, dir_vec_name=name),), + densities=(1,), + kernel_arguments=kernel_arguments) + return res + + def merge_int_g_expr(expr): have_int_g = HaveIntGs() if have_int_g(expr) <= 1: return expr try: - res = sum(_merge_int_g_expr(expr)) - return res + result_coeff, result_int_g = _merge_int_g_expr(expr) + result_int_g = _convert_axis_source_to_directional_source(result_int_g) + return result_coeff + result_int_g except AssertionError: return expr +def _merge_source_kernel_duplicates(source_kernels, densities): + new_source_kernels = [] + new_densities = [] + for knl, density in zip(source_kernels, densities): + if knl not in new_source_kernels: + new_source_kernels.append(knl) + new_densities.append(density) + else: + idx = new_source_kernels.index(knl) + new_densities[idx] += density + return new_source_kernels, new_densities + + +def _merge_kernel_arguments(x, y): + res = x.copy() + for k, v in y.items(): + if k in res: + assert res[k] == v + else: + res[k] = v + return res + + def _merge_int_g_expr(expr): if isinstance(expr, Sum): result_coeff = 0 @@ -167,11 +235,17 @@ def _merge_int_g_expr(expr): assert result_int_g.source == int_g.source assert result_int_g.target == int_g.target assert result_int_g.qbx_forced_limit == int_g.qbx_forced_limit - assert result_int_g.kernel_arguments == int_g.kernel_arguments assert result_int_g.target_kernel == int_g.target_kernel + kernel_arguments = _merge_kernel_arguments(result_int_g.kernel_arguments, + int_g.kernel_arguments) + source_kernels = result_int_g.source_kernels + int_g.source_kernels + densities = result_int_g.densities + int_g.densities + new_source_kernels, new_densities = \ + _merge_source_kernel_duplicates(source_kernels, densities) result_int_g = result_int_g.copy( - source_kernels=(result_int_g.source_kernels + int_g.source_kernels), - densities=(result_int_g.densities + int_g.densities) + source_kernels=tuple(new_source_kernels), + densities=tuple(new_densities), + kernel_arguments=kernel_arguments, ) return result_coeff, result_int_g elif isinstance(expr, Product): @@ -192,13 +266,15 @@ def _merge_int_g_expr(expr): new_densities = (density * mult for density in new_int_g.densities) return coeff*mult, new_int_g.copy(densities=new_densities) elif isinstance(expr, IntG): - return 0, expr + return 0, _convert_target_deriv_to_source(expr) else: return expr, 0 + if __name__ == "__main__": from sumpy.kernel import StokesletKernel, BiharmonicKernel, StressletKernel - base_kernel = BiharmonicKernel(2) - kernels = [StokesletKernel(2, 0, 1), StokesletKernel(2, 0, 0)] - kernels = [StressletKernel(2, 0, 1, 0)] - get_deriv_relation(kernels, base_kernel, tol=1e-10, order=3, verbose=True) + base_kernel = BiharmonicKernel(3) + kernels = [StokesletKernel(3, 0, 1), StokesletKernel(3, 0, 0)] + kernels = [StressletKernel(3, 0, 1, 0), StressletKernel(3, 0, 0, 0), + StressletKernel(3, 0, 1, 2)] + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=2, verbose=True) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 05caca752..c9a1f84fb 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -25,7 +25,8 @@ from pytential import sym from pytential.symbolic.pde.system_utils import merge_int_g_exprs from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, - AxisTargetDerivative, AxisSourceDerivative, BiharmonicKernel) + AxisTargetDerivative, AxisSourceDerivative, BiharmonicKernel, + TargetPointMultiplier) __doc__ = """ .. autoclass:: StokesletWrapper @@ -66,6 +67,8 @@ class StokesletWrapperBase: .. automethod:: apply_derivative .. automethod:: apply_stress """ + def __init__(self, dim): + self.dim = dim def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): """Symbolic expressions for integrating Stokeslet kernel. @@ -83,7 +86,16 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): """Symbolic expression for pressure field associated with the Stokeslet.""" - raise NotImplementedError + from pytential.symbolic.mappers import DerivativeTaker + kernel = LaplaceKernel(dim=self.dim) + sym_expr = 0 + + for i in range(self.dim): + sym_expr += (DerivativeTaker(i).map_int_g( + sym.S(kernel, density_vec_sym[i], + qbx_forced_limit=qbx_forced_limit))) + + return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, mu_sym, qbx_forced_limit): @@ -155,6 +167,8 @@ class StressletWrapperBase: .. automethod:: apply_derivative .. automethod:: apply_stress """ + def __init__(self, dim): + self.dim = dim def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): """Symbolic expressions for integrating Stresslet kernel. @@ -173,7 +187,22 @@ def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): """Symbolic expression for pressure field associated with the Stresslet.""" - raise NotImplementedError + import itertools + from pytential.symbolic.mappers import DerivativeTaker + kernel = LaplaceKernel(dim=self.dim) + + factor = (2. * mu_sym) + + sym_expr = 0 + + for i, j in itertools.product(range(self.dim), range(self.dim)): + sym_expr += factor * DerivativeTaker(i).map_int_g( + DerivativeTaker(j).map_int_g( + sym.S(kernel, + density_vec_sym[i] * dir_vec_sym[j], + qbx_forced_limit=qbx_forced_limit))) + + return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): @@ -269,7 +298,7 @@ def create_int_g(knl, deriv_dirs, **kwargs): class StokesletWrapper(StokesletWrapperBase, StokesletWrapperMixin): def __init__(self, dim=None, use_biharmonic=True, use_source=True): - self.dim = dim + super().__init__(dim) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -311,19 +340,6 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): return sym_expr - def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): - - from pytential.symbolic.mappers import DerivativeTaker - kernel = LaplaceKernel(dim=self.dim) - sym_expr = 0 - - for i in range(self.dim): - sym_expr += (DerivativeTaker(i).map_int_g( - sym.S(kernel, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit))) - - return sym_expr - def apply_derivative(self, deriv_dir, density_vec_sym, mu_sym, qbx_forced_limit): @@ -388,11 +404,11 @@ class StressletWrapper(StressletWrapperBase, StokesletWrapperMixin): """ def __init__(self, dim=None, use_biharmonic=True, use_source=True): - self.use_biharmonic = use_biharmonic - self.dim = dim + super().__init__(dim) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") + self.use_biharmonic = use_biharmonic self.kernel_dict = {} self.base_kernel = BiharmonicKernel(dim=dim) @@ -436,25 +452,6 @@ def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): return sym_expr - def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): - - import itertools - from pytential.symbolic.mappers import DerivativeTaker - kernel = LaplaceKernel(dim=self.dim) - - factor = (2. * mu_sym) - - sym_expr = 0 - - for i, j in itertools.product(range(self.dim), range(self.dim)): - sym_expr += factor * DerivativeTaker(i).map_int_g( - DerivativeTaker(j).map_int_g( - sym.S(kernel, - density_vec_sym[i] * dir_vec_sym[j], - qbx_forced_limit=qbx_forced_limit))) - - return sym_expr - def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): @@ -500,6 +497,110 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # }}} +# {{{ Stokeslet/Stresslet using Laplace + +class StokesletWrapperUsingLaplace(StokesletWrapperBase, StokesletWrapperMixin): + """Stokeslet Wrapper using Tornberg and Greengard's method which uses + Laplace derivatives. + + [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the + three-dimensional Stokes equations. + Journal of Computational Physics, 227(3), 1613-1619. + """ + def __init__(self, dim=None): + self.dim = dim + if dim != 3: + raise ValueError("unsupported dimension given to StokesletWrapper") + self.kernel = LaplaceKernel(dim=self.dim) + + def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): + + sym_expr = np.zeros((self.dim,), dtype=object) + + source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + common_expr_density = sum(source[k]*density_vec_sym[k] for + k in range(self.dim)) + + for i in range(self.dim): + for j in range(self.dim): + knl = TargetPointMultiplier(j, AxisTargetDerivative(i, self.kernel)) + sym_expr[i] -= sym.S(knl, density_vec_sym[j], + qbx_forced_limit=qbx_forced_limit) + if i == j: + sym_expr[i] += sym.S(self.kernel, density_vec_sym[j], + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += sym.S(AxisTargetDerivative(i, self.kernel), + common_expr_density, qbx_forced_limit=qbx_forced_limit) + sym_expr[i] *= -0.5*(mu_sym**(-1)) + + print(sym_expr[0]) + return sym_expr + + +class StressletWrapperUsingLaplace(StokesletWrapperBase, StokesletWrapperMixin): + """Stresslet Wrapper using Tornberg and Greengard's method which uses + Laplace derivatives. + + [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the + three-dimensional Stokes equations. + Journal of Computational Physics, 227(3), 1613-1619. + """ + def __init__(self, dim=None): + self.dim = dim + if dim != 3: + raise ValueError("unsupported dimension given to StressletWrapper") + self.kernel = LaplaceKernel(dim=self.dim) + + def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + + sym_expr = np.zeros((self.dim,), dtype=object) + + source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + + for i in range(self.dim): + for j in range(self.dim): + source_kernels = [AxisSourceDerivative(k, self.kernel) for + k in range(self.dim)] + densities = [density_vec_sym[k] * dir_vec_sym[j] + + density_vec_sym[j] * dir_vec_sym[k] + for k in range(self.dim)] + target_kernel = TargetPointMultiplier(j, + AxisTargetDerivative(i, self.kernel)) + sym_expr[i] -= sym.IntG(target_kernel=target_kernel, + source_kernels=source_kernels, + densities=densities, + qbx_forced_limit=qbx_forced_limit) + + if i == j: + sym_expr[i] += sym.IntG(target_kernel=self.kernel, + source_kernels=source_kernels, + densities=densities, + qbx_forced_limit=qbx_forced_limit) + + common_density0 = sum(source[k] * density_vec_sym[k] for + k in range(self.dim)) + common_density1 = sum(source[k] * dir_vec_sym[k] for + k in range(self.dim)) + source_kernels = [AxisSourceDerivative(k, self.kernel) for + k in range(self.dim)] + densities = [common_density0 * dir_vec_sym[k] + + common_density1 * density_vec_sym[k] for + k in range(self.dim)] + + target_kernel = AxisTargetDerivative(i, self.kernel) + sym_expr[i] += sym.IntG(target_kernel=target_kernel, + source_kernels=source_kernels, + densities=densities, + qbx_forced_limit=qbx_forced_limit) + + sym_expr[i] *= 3.0/6 + + return sym_expr + + +# }}} + + # {{{ base Stokes operator class StokesOperator: @@ -516,7 +617,7 @@ class StokesOperator: .. automethod:: pressure """ - def __init__(self, ambient_dim, side, use_biharmonic): + def __init__(self, ambient_dim, side, method="naive"): """ :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. @@ -528,11 +629,18 @@ def __init__(self, ambient_dim, side, use_biharmonic): self.ambient_dim = ambient_dim self.side = side - self.stresslet = StressletWrapper(dim=self.ambient_dim, + if method == "laplace": + self.stresslet = StressletWrapperUsingLaplace(dim=self.ambient_dim) + self.stokeslet = StokesletWrapperUsingLaplace(dim=self.ambient_dim) + elif method == "biharmonic" or method == "naive": + use_biharmonic = (method == "biharmonic") + self.stresslet = StressletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic) - self.stokeslet = StokesletWrapper(dim=self.ambient_dim, + self.stokeslet = StokesletWrapper(dim=self.ambient_dim, use_biharmonic=use_biharmonic) - self.use_biharmonic = use_biharmonic + else: + raise ValueError(f"invalid method: {method}." + "Needs to be one of naive, laplace, biharmonic") @property def dim(self): @@ -588,7 +696,7 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=None, eta=None, use_biharmonic=False): + def __init__(self, *, omega, alpha=None, eta=None, method="naive"): r""" :arg omega: farfield behaviour of the velocity field, as defined by :math:`A` in [HsiaoKress1985]_ Equation 2.3. @@ -596,7 +704,7 @@ def __init__(self, *, omega, alpha=None, eta=None, use_biharmonic=False): :arg eta: real parameter :math:`\eta > 0`. Choosing this parameter well can have a non-trivial effect on the conditioning. """ - super().__init__(ambient_dim=2, side=+1, use_biharmonic=use_biharmonic) + super().__init__(ambient_dim=2, side=+1, method=method) # NOTE: in [hsiao-kress], there is an analysis on a circle, which # recommends values in @@ -679,13 +787,13 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None, use_biharmonic=False): + def __init__(self, *, eta=None, method="naive"): r""" :arg eta: a parameter :math:`\eta > 0`. Choosing this parameter well can have a non-trivial effect on the conditioning of the operator. """ - super().__init__(ambient_dim=3, side=+1, use_biharmonic=use_biharmonic) + super().__init__(ambient_dim=3, side=+1, method=method) # NOTE: eta is chosen here based on H. 1986 Figure 1, which is # based on solving on the unit sphere diff --git a/test/test_maxwell.py b/test/test_maxwell.py index 8bdcd41a7..57b52b7ba 100644 --- a/test/test_maxwell.py +++ b/test/test_maxwell.py @@ -351,7 +351,10 @@ def eval_inc_field_at(places, source=None, target=None): inc_xyz_sym = EHField(sym.make_sym_vector("inc_fld", 6)) - bound_j_op = bind(places, mfie.j_operator(loc_sign, jt_sym)) + mfie_j = mfie.j_operator(loc_sign, jt_sym) + from pytential.symbolic.pde.system_utils import merge_int_g_exprs + mfie_j = merge_int_g_exprs(mfie_j) + bound_j_op = bind(places, mfie_j) j_rhs = bind(places, mfie.j_rhs(inc_xyz_sym.h))( actx, inc_fld=inc_field_scat.field, **knl_kwargs) diff --git a/test/test_stokes.py b/test/test_stokes.py index 71bb76895..8a59b4710 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -46,17 +46,18 @@ def run_exterior_stokes(ctx_factory, *, ambient_dim, target_order, qbx_order, resolution, - fmm_order=False, # FIXME: FMM is slower than direct evaluation + fmm_order=None, # FIXME: FMM is slower than direct evaluation source_ovsmp=None, radius=1.5, mu=1.0, visualize=False, - use_biharmonic=False, + method="naive", _target_association_tolerance=0.05, _expansions_in_tree_have_extent=True): cl_ctx = cl.create_some_context() - queue = cl.CommandQueue(cl_ctx) + queue = cl.CommandQueue(cl_ctx, + properties=cl.command_queue_properties.PROFILING_ENABLE) actx = PyOpenCLArrayContext(queue) # {{{ geometry @@ -135,10 +136,10 @@ def run_exterior_stokes(ctx_factory, *, if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator sym_omega = sym.make_sym_vector("omega", ambient_dim) - op = HsiaoKressExteriorStokesOperator(omega=sym_omega, use_biharmonic=use_biharmonic) + op = HsiaoKressExteriorStokesOperator(omega=sym_omega, method=method) elif ambient_dim == 3: from pytential.symbolic.stokes import HebekerExteriorStokesOperator - op = HebekerExteriorStokesOperator(use_biharmonic=use_biharmonic) + op = HebekerExteriorStokesOperator(method=method) else: assert False @@ -151,7 +152,8 @@ def run_exterior_stokes(ctx_factory, *, sym_velocity = op.velocity(sym_sigma, normal=sym_normal, mu=sym_mu) from pytential.symbolic.stokes import StokesletWrapper - sym_source_pot = StokesletWrapper(ambient_dim, use_biharmonic=False).apply(sym_sigma, sym_mu, qbx_forced_limit=None) + sym_source_pot = StokesletWrapper(ambient_dim, + use_biharmonic=False).apply(sym_sigma, sym_mu, qbx_forced_limit=None) # }}} @@ -187,14 +189,18 @@ def run_exterior_stokes(ctx_factory, *, # }}} - from time import time - scipy_op = bound_op.scipy_op(actx, "sigma", np.float64, **op_context) - res = scipy_op.matvec(rhs) - print("start") - start = time() - res = scipy_op.matvec(rhs) - print(time()-start) - + fmm_timing_data = {} + bound_op.eval({"sigma": rhs, **op_context}, array_context=actx, + timing_data=fmm_timing_data) + + def print_timing_data(timings, name): + result = {k: 0 for k in list(timings.values())[0].keys()} + for k, timing in timings.items(): + for k, v in timing.items(): + result[k] += v['wall_elapsed'] + print(f"{name}={result}") + + print_timing_data(fmm_timing_data, method) # {{{ solve from pytential.solve import gmres @@ -207,7 +213,6 @@ def run_exterior_stokes(ctx_factory, *, progress=visualize, stall_iterations=0, hard_failure=True) - sigma = result.solution # }}} @@ -261,7 +266,7 @@ def rnorm2(x, y): 2, pytest.param(3, marks=pytest.mark.slowtest) ]) -def test_exterior_stokes(ctx_factory, ambient_dim, visualize=False, use_biharmonic=False): +def test_exterior_stokes(ctx_factory, ambient_dim, visualize=False, method="naive"): if visualize: logging.basicConfig(level=logging.INFO) @@ -288,7 +293,7 @@ def test_exterior_stokes(ctx_factory, ambient_dim, visualize=False, use_biharmon qbx_order=qbx_order, resolution=resolution, visualize=visualize, - use_biharmonic=use_biharmonic) + method=method) eoc.add_data_point(h_max, err) From 64ee9a4ac9b666069c19ec885fa6936befeb95b9 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 2 May 2021 13:58:20 -0500 Subject: [PATCH 034/209] Revert "print timing" This reverts commit 6f458826cc484f9dd97e8f01fc5364375620d562. --- test/test_layer_pot.py | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/test/test_layer_pot.py b/test/test_layer_pot.py index ae5e0712c..9a6f547b0 100644 --- a/test/test_layer_pot.py +++ b/test/test_layer_pot.py @@ -152,14 +152,14 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): logging.basicConfig(level=logging.INFO) cl_ctx = ctx_factory() - queue = cl.CommandQueue(cl_ctx, properties=cl.command_queue_properties.PROFILING_ENABLE) + queue = cl.CommandQueue(cl_ctx) actx = PyOpenCLArrayContext(queue) # prevent cache 'splosion from sympy.core.cache import clear_cache clear_cache() - nelements = 30000*3 + nelements = 300 target_order = 8 qbx_order = 3 @@ -177,13 +177,11 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): direct_qbx = QBXLayerPotentialSource( pre_density_discr, 4*target_order, qbx_order, fmm_order=False, - fmm_backend="sumpy", target_association_tolerance=0.05, ) fmm_qbx = QBXLayerPotentialSource( pre_density_discr, 4*target_order, qbx_order, fmm_order=qbx_order + 3, - fmm_backend="sumpy", _expansions_in_tree_have_extent=True, target_association_tolerance=0.05, ) @@ -208,9 +206,9 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): op = op_d + op_s * 0.5 try: direct_sigma = direct_density_discr.zeros(actx) + 1 - direct_fld_in_vol = 0 #bind(places, op, - # auto_where=("direct_qbx", "target"))( - # actx, sigma=direct_sigma) + direct_fld_in_vol = bind(places, op, + auto_where=("direct_qbx", "target"))( + actx, sigma=direct_sigma) except QBXTargetAssociationFailedException as e: fplot.show_scalar_in_matplotlib( actx.to_numpy(actx.thaw(e.failed_target_flags))) @@ -220,21 +218,10 @@ def test_off_surface_eval_vs_direct(ctx_factory, do_plot=False): fmm_sigma = fmm_density_discr.zeros(actx) + 1 fmm_bound_op = bind(places, op, auto_where=("fmm_qbx", "target")) - #print(fmm_bound_op.code) - fmm_timing_data = {} - fmm_fld_in_vol = fmm_bound_op.eval({"sigma": fmm_sigma}, array_context=actx, timing_data=fmm_timing_data) - err = actx.np.fabs(fmm_fld_in_vol - direct_fld_in_vol) - - def print_timing_data(timing_data): - timings = list(fmm_timing_data.values()) - result = {k: 0 for k in timings[0].keys()} - for timing in timings: - for k, v in timing.items(): - result[k] += v['wall_elapsed'] - print(result) - - print_timing_data(fmm_timing_data) + print(fmm_bound_op.code) + fmm_fld_in_vol = fmm_bound_op(actx, sigma=fmm_sigma) + err = actx.np.fabs(fmm_fld_in_vol - direct_fld_in_vol) linf_err = actx.to_numpy(err).max() print("l_inf error:", linf_err) @@ -245,18 +232,16 @@ def print_timing_data(timing_data): ("direct_fld_in_vol", actx.to_numpy(direct_fld_in_vol)) ]) - #assert linf_err < 1e-3 + assert linf_err < 1e-3 # check that using one FMM works op = op_d.copy(source_kernels=op_d.source_kernels + (knl,), densities=op_d.densities + (sym.var("sigma")*0.5,)) single_fmm_bound_op = bind(places, op, auto_where=("fmm_qbx", "target")) - fmm_timing_data = {} - single_fmm_fld_in_vol = single_fmm_bound_op.eval({"sigma": fmm_sigma}, array_context=actx, timing_data=fmm_timing_data) - err = actx.np.fabs(single_fmm_fld_in_vol - direct_fld_in_vol) - - print_timing_data(fmm_timing_data) + print(single_fmm_bound_op.code) + single_fmm_fld_in_vol = fmm_bound_op(actx, sigma=fmm_sigma) + err = actx.np.fabs(fmm_fld_in_vol - single_fmm_fld_in_vol) linf_err = actx.to_numpy(err).max() print("l_inf error:", linf_err) From 0c40dc1c27760148976f1435c84f4820ac340ee6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 2 May 2021 14:30:44 -0500 Subject: [PATCH 035/209] Remove debug print --- pytential/symbolic/stokes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index c9a1f84fb..f8780373a 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -533,7 +533,6 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): common_expr_density, qbx_forced_limit=qbx_forced_limit) sym_expr[i] *= -0.5*(mu_sym**(-1)) - print(sym_expr[0]) return sym_expr From 3ef69fe3c6a8a09a32b3a60bdce1ab7802c6295f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 2 May 2021 15:43:06 -0500 Subject: [PATCH 036/209] Add a test for biharmonic stokes as well --- test/test_stokes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 8a59b4710..83d478449 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -262,11 +262,12 @@ def rnorm2(x, y): return h_max, v_error +@pytest.mark.parametrize("method", ["naive", "biharmonic"]) @pytest.mark.parametrize("ambient_dim", [ 2, pytest.param(3, marks=pytest.mark.slowtest) ]) -def test_exterior_stokes(ctx_factory, ambient_dim, visualize=False, method="naive"): +def test_exterior_stokes(ctx_factory, ambient_dim, method="naive", visualize=False): if visualize: logging.basicConfig(level=logging.INFO) From 2b20fd88808865d5eb2666fd5a9bd4f90782cee0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 6 May 2021 12:26:12 -0500 Subject: [PATCH 037/209] Use LineTaylor even for target derivatives --- pytential/qbx/__init__.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pytential/qbx/__init__.py b/pytential/qbx/__init__.py index c63a0edb0..324a87f0c 100644 --- a/pytential/qbx/__init__.py +++ b/pytential/qbx/__init__.py @@ -689,15 +689,7 @@ def exec_compute_potential_insn_fmm(self, actx: PyOpenCLArrayContext, @memoize_method def get_expansion_for_qbx_direct_eval(self, base_kernel, target_kernels): from sumpy.expansion.local import LineTaylorLocalExpansion - from sumpy.kernel import TargetDerivativeRemover - - # line Taylor cannot support target derivatives - tdr = TargetDerivativeRemover() - if any(knl != tdr(knl) for knl in target_kernels): - return self.expansion_factory.get_local_expansion_class( - base_kernel)(base_kernel, self.qbx_order) - else: - return LineTaylorLocalExpansion(base_kernel, self.qbx_order) + return LineTaylorLocalExpansion(base_kernel, self.qbx_order) @memoize_method def get_lpot_applier(self, target_kernels, source_kernels): From 861d596ec4ae2364a2d971282056d48ddeaf1f98 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 9 May 2021 16:48:59 -0500 Subject: [PATCH 038/209] Initial support for ElasticityKernel --- experiments/stokes-2d-interior.py | 16 +- pytential/symbolic/pde/system_utils.py | 4 +- pytential/symbolic/stokes.py | 222 +++++++++++++------------ test/test_stokes.py | 18 +- 4 files changed, 136 insertions(+), 124 deletions(-) diff --git a/experiments/stokes-2d-interior.py b/experiments/stokes-2d-interior.py index bcb15fde8..44828a6b1 100644 --- a/experiments/stokes-2d-interior.py +++ b/experiments/stokes-2d-interior.py @@ -87,10 +87,10 @@ def get_obj_array(obj_array): loc_sign = -1 # Create stresslet object - stresslet_obj = StressletWrapper(dim=2) + stresslet_obj = StressletWrapper(dim=2, mu_sym=mu_sym) # Describe boundary operator - bdry_op_sym = loc_sign * 0.5 * sigma_sym + sqrt_w * stresslet_obj.apply(inv_sqrt_w_sigma, nvec_sym, mu_sym, qbx_forced_limit='avg') + bdry_op_sym = loc_sign * 0.5 * sigma_sym + sqrt_w * stresslet_obj.apply(inv_sqrt_w_sigma, nvec_sym, qbx_forced_limit='avg') # Bind to the qbx discretization bound_op = bind(qbx, bdry_op_sym) @@ -140,7 +140,7 @@ def couette_soln(x, y, dp, h): sigma = gmres_result.solution # Describe representation of solution for evaluation in domain - representation_sym = stresslet_obj.apply(inv_sqrt_w_sigma, nvec_sym, mu_sym, qbx_forced_limit=-2) + representation_sym = stresslet_obj.apply(inv_sqrt_w_sigma, nvec_sym, qbx_forced_limit=-2) from sumpy.visualization import FieldPlotter nsamp = 10 @@ -194,11 +194,11 @@ def stride_hack(arr): print("exact velocity at max error points: x -> ", err[0][max_error_loc[0]], ", y -> ", err[1][max_error_loc[1]]) from pytential.symbolic.mappers import DerivativeTaker - rep_pressure = stresslet_obj.apply_pressure(inv_sqrt_w_sigma, nvec_sym, mu_sym, qbx_forced_limit=-2) + rep_pressure = stresslet_obj.apply_pressure(inv_sqrt_w_sigma, nvec_sym, qbx_forced_limit=-2) pressure = bind((qbx, PointsTarget(eval_points_dev)), rep_pressure)(queue, sigma=sigma, mu=mu, normal=normal) pressure = pressure.get() - print "pressure = ", pressure + print(f"pressure = ${pressure}") x_dir_vecs = np.zeros((2,len(eval_points[0]))) x_dir_vecs[0,:] = 1.0 @@ -207,7 +207,7 @@ def stride_hack(arr): x_dir_vecs = cl.array.to_device(queue, x_dir_vecs) y_dir_vecs = cl.array.to_device(queue, y_dir_vecs) dir_vec_sym = sym.make_sym_vector("force_direction", dim) - rep_stress = stresslet_obj.apply_stress(inv_sqrt_w_sigma, nvec_sym, dir_vec_sym, mu_sym, qbx_forced_limit=-2) + rep_stress = stresslet_obj.apply_stress(inv_sqrt_w_sigma, nvec_sym, dir_vec_sym, qbx_forced_limit=-2) applied_stress_x = bind((qbx, PointsTarget(eval_points_dev)), rep_stress)(queue, sigma=sigma, normal=normal, force_direction=x_dir_vecs, mu=mu) @@ -216,8 +216,8 @@ def stride_hack(arr): rep_stress)(queue, sigma=sigma, normal=normal, force_direction=y_dir_vecs, mu=mu) applied_stress_y = get_obj_array(applied_stress_y) - print "stress applied to x direction: ", applied_stress_x - print "stress applied to y direction: ", applied_stress_y + print(f"stress applied to x direction: ${applied_stress_x}") + print(f"stress applied to y direction: ${applied_stress_y}") import matplotlib.pyplot as plt diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index ff1b7a304..0f4ec8ae3 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -272,9 +272,11 @@ def _merge_int_g_expr(expr): if __name__ == "__main__": - from sumpy.kernel import StokesletKernel, BiharmonicKernel, StressletKernel + from sumpy.kernel import (StokesletKernel, BiharmonicKernel, StressletKernel, + ElasticityKernel) base_kernel = BiharmonicKernel(3) kernels = [StokesletKernel(3, 0, 1), StokesletKernel(3, 0, 0)] kernels = [StressletKernel(3, 0, 1, 0), StressletKernel(3, 0, 0, 0), StressletKernel(3, 0, 1, 2)] + kernels = [ElasticityKernel(3, 0, 1), ElasticityKernel(3, 0, 0)] get_deriv_relation(kernels, base_kernel, tol=1e-10, order=2, verbose=True) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index f1dcf20ce..b6b80c5ca 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -25,8 +25,9 @@ from pytential import sym from pytential.symbolic.pde.system_utils import merge_int_g_exprs from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, - AxisTargetDerivative, AxisSourceDerivative, BiharmonicKernel, - TargetPointMultiplier) + ElasticityKernel, BiharmonicKernel, + AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) +from pymbolic import var __doc__ = """ .. autoclass:: StokesletWrapper @@ -67,10 +68,12 @@ class StokesletWrapperBase: .. automethod:: apply_derivative .. automethod:: apply_stress """ - def __init__(self, dim): + def __init__(self, dim, mu_sym, nu_sym): self.dim = dim + self.mu = mu_sym + self.nu = nu_sym - def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): + def apply(self, density_vec_sym, qbx_forced_limit): """Symbolic expressions for integrating Stokeslet kernel. Returns an object array of symbolic expressions for the vector @@ -78,13 +81,12 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): variable *density_vec_sym*. :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg mu_sym: a symbolic variable for the viscosity. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ raise NotImplementedError - def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): + def apply_pressure(self, density_vec_sym, qbx_forced_limit): """Symbolic expression for pressure field associated with the Stokeslet.""" from pytential.symbolic.mappers import DerivativeTaker kernel = LaplaceKernel(dim=self.dim) @@ -97,8 +99,7 @@ def apply_pressure(self, density_vec_sym, mu_sym, qbx_forced_limit): return sym_expr - def apply_derivative(self, deriv_dir, density_vec_sym, - mu_sym, qbx_forced_limit): + def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): """Symbolic derivative of velocity from Stokeslet. Returns an object array of symbolic expressions for the vector @@ -107,14 +108,12 @@ def apply_derivative(self, deriv_dir, density_vec_sym, :arg deriv_dir: integer denoting the axis direction for the derivative. :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg mu_sym: a symbolic variable for the viscosity. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ raise NotImplementedError - def apply_stress(self, density_vec_sym, dir_vec_sym, - mu_sym, qbx_forced_limit): + def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. Returns a vector of symbolic expressions for the force resulting @@ -134,7 +133,6 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, :arg density_vec_sym: a symbolic vector variable for the density vector. :arg dir_vec_sym: a symbolic vector for the application direction. - :arg mu_sym: a symbolic variable for the viscosity. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ @@ -167,10 +165,12 @@ class StressletWrapperBase: .. automethod:: apply_derivative .. automethod:: apply_stress """ - def __init__(self, dim): + def __init__(self, dim, mu_sym, nu_sym): self.dim = dim + self.mu = mu_sym + self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): """Symbolic expressions for integrating Stresslet kernel. Returns an object array of symbolic expressions for the vector @@ -179,19 +179,18 @@ def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): :arg density_vec_sym: a symbolic vector variable for the density vector. :arg dir_vec_sym: a symbolic vector variable for the direction vector. - :arg mu_sym: a symbolic variable for the viscosity. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ raise NotImplementedError - def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): """Symbolic expression for pressure field associated with the Stresslet.""" import itertools from pytential.symbolic.mappers import DerivativeTaker kernel = LaplaceKernel(dim=self.dim) - factor = (2. * mu_sym) + factor = (2. * self.mu) sym_expr = 0 @@ -205,7 +204,7 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit) return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, - mu_sym, qbx_forced_limit): + qbx_forced_limit): """Symbolic derivative of velocity from stresslet. Returns an object array of symbolic expressions for the vector @@ -216,14 +215,13 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, :arg deriv_dir: integer denoting the axis direction for the derivative. :arg density_vec_sym: a symbolic vector variable for the density vector. :arg dir_vec_sym: a symbolic vector variable for the normal direction. - :arg mu_sym: a symbolic variable for the viscosity. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ raise NotImplementedError def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, - mu_sym, qbx_forced_limit): + qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. Returns a vector of symbolic expressions for the force resulting @@ -239,7 +237,6 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, :arg normal_vec_sym: a symbolic vector variable for the normal vectors (outward facing normals at source locations). :arg dir_vec_sym: a symbolic vector for the application direction. - :arg mu_sym: a symbolic variable for the viscosity. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ @@ -250,7 +247,7 @@ class StokesletWrapperMixin: """A base class for StokesletWrapper and StressletWrapper to create IntG instances """ - def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, + def get_int_g(self, idx, density, dir_vec_sym, qbx_forced_limit, deriv_dirs): """ @@ -261,21 +258,20 @@ def get_int_g(self, idx, density, mu_sym, qbx_forced_limit, def create_int_g(knl, deriv_dirs, **kwargs): for deriv_dir in deriv_dirs: - if self.use_source: - knl = AxisSourceDerivative(deriv_dir, knl) - else: - knl = AxisTargetDerivative(deriv_dir, knl) + knl = AxisSourceDerivative(deriv_dir, knl) - res = sym.S(knl, density, + args = [arg.loopy_arg.name for arg in knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) + + res = sym.S(knl, density*dir_vec_sym[idx[-1]], qbx_forced_limit=qbx_forced_limit, **kwargs) - if self.use_source: - return res*(-1)**len(deriv_dirs) - else: - return res + + return res*(-1)**len(deriv_dirs) if not self.use_biharmonic: knl = self.kernel_dict[idx] - return create_int_g(knl, deriv_dirs, mu=mu_sym) + return create_int_g(knl, deriv_dirs) deriv_relation = self.deriv_relation_dict[idx] const = deriv_relation[0] @@ -284,21 +280,22 @@ def create_int_g(knl, deriv_dirs, **kwargs): # on the source instead of the target when using automatic tagging # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) - const *= sym.integral(self.dim, self.dim-1, density, dofdesc=dd) + const *= sym.integral(self.dim, self.dim-1, density*dir_vec_sym[idx[-1]], + dofdesc=dd) result = const for mi, coeff in deriv_relation[1]: new_deriv_dirs = list(deriv_dirs) - for idx, val in enumerate(mi): - new_deriv_dirs.extend([idx]*val) + for i, val in enumerate(mi): + new_deriv_dirs.extend([i]*val) result += create_int_g(self.base_kernel, new_deriv_dirs) * coeff return result class StokesletWrapper(StokesletWrapperBase, StokesletWrapperMixin): - def __init__(self, dim=None, use_biharmonic=True, use_source=True): - super().__init__(dim) + def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): + super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -307,12 +304,11 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.kernel_dict = {} self.base_kernel = BiharmonicKernel(dim=dim) - self.use_source = use_source for i in range(dim): for j in range(i, dim): - self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, - jcomp=j) + self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, + jcomp=j, viscosity_mu=mu_sym, poisson_ratio=nu_sym) # The dictionary allows us to exploit symmetry -- that # :math:`T_{01}` is identical to :math:`T_{10}` -- and avoid creating @@ -329,44 +325,50 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): self.deriv_relation_dict[idx] = deriv_eq - def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): + def apply(self, density_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) + # For stokeslet, there's no direction vector involved + # passing a list of ones instead to remove its usage. for comp in range(self.dim): for i in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i), - density_vec_sym[i], mu_sym, qbx_forced_limit, deriv_dirs=[]) + density_vec_sym[i], [1]*self.dim, + qbx_forced_limit, deriv_dirs=[]) return sym_expr - def apply_derivative(self, deriv_dir, density_vec_sym, - mu_sym, qbx_forced_limit): + def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): - sym_expr = self.apply(density_vec_sym, mu_sym, qbx_forced_limit) + sym_expr = self.apply(density_vec_sym, qbx_forced_limit) + # For stokeslet, there's no direction vector involved + # passing a list of ones instead to remove its usage. for comp in range(self.dim): for i in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i), - density_vec_sym[i], mu_sym, qbx_forced_limit, - deriv_dirs=[deriv_dir]) + density_vec_sym[i], [1]*self.dim, + qbx_forced_limit, deriv_dirs=[deriv_dir]) return sym_expr - def apply_stress(self, density_vec_sym, dir_vec_sym, - mu_sym, qbx_forced_limit): + def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = StressletWrapper(dim=self.dim, - use_biharmonic=self.use_biharmonic) + use_biharmonic=self.use_biharmonic, + mu_sym=self.mu, nu_sym=self.nu) + # For stokeslet, there's no direction vector involved + # passing a list of ones instead to remove its usage. for comp in range(self.dim): for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += dir_vec_sym[i] * \ stresslet_obj.get_int_g((comp, i, j), - density_vec_sym[j], - mu_sym, qbx_forced_limit, deriv_dirs=[]) + density_vec_sym[j], [1]*self.dim, + qbx_forced_limit, deriv_dirs=[]) return sym_expr @@ -403,8 +405,8 @@ class StressletWrapper(StressletWrapperBase, StokesletWrapperMixin): .. automethod:: apply_stress """ - def __init__(self, dim=None, use_biharmonic=True, use_source=True): - super().__init__(dim) + def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): + super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -412,13 +414,12 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): self.kernel_dict = {} self.base_kernel = BiharmonicKernel(dim=dim) - self.use_source = use_source for i in range(dim): for j in range(i, dim): for k in range(j, dim): self.kernel_dict[(i, j, k)] = StressletKernel(dim=dim, icomp=i, - jcomp=j, kcomp=k) + jcomp=j, kcomp=k) # The dictionary allows us to exploit symmetry -- that # :math:`T_{012}` is identical to :math:`T_{120}` -- and avoid creating @@ -439,7 +440,7 @@ def __init__(self, dim=None, use_biharmonic=True, use_source=True): for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): self.deriv_relation_dict[idx] = deriv_eq - def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) @@ -447,13 +448,13 @@ def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i, j), - dir_vec_sym[i] * density_vec_sym[j], - mu_sym, qbx_forced_limit, deriv_dirs=[]) + dir_vec_sym[i], density_vec_sym, + qbx_forced_limit, deriv_dirs=[]) return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, - mu_sym, qbx_forced_limit): + qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) @@ -461,13 +462,13 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i, j), - dir_vec_sym[i] * density_vec_sym[j], - mu_sym, qbx_forced_limit, deriv_dirs=[deriv_dir]) + dir_vec_sym[i], density_vec_sym, + qbx_forced_limit, deriv_dirs=[deriv_dir]) return sym_expr def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, - mu_sym, qbx_forced_limit): + qbx_forced_limit): sym_expr = np.empty((self.dim,), dtype=object) @@ -475,19 +476,19 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, sym_grad_matrix = np.empty((self.dim, self.dim), dtype=object) for i in range(self.dim): sym_grad_matrix[:, i] = self.apply_derivative(i, density_vec_sym, - normal_vec_sym, mu_sym, qbx_forced_limit) + normal_vec_sym, qbx_forced_limit) for comp in range(self.dim): # First, add the pressure term: sym_expr[comp] = - dir_vec_sym[comp] * self.apply_pressure( density_vec_sym, normal_vec_sym, - mu_sym, qbx_forced_limit) + qbx_forced_limit) # Now add the velocity derivative components for j in range(self.dim): sym_expr[comp] = sym_expr[comp] + ( - dir_vec_sym[j] * mu_sym * ( + dir_vec_sym[j] * self.mu * ( sym_grad_matrix[comp][j] + sym_grad_matrix[j][comp]) ) @@ -507,13 +508,15 @@ class StokesletWrapperUsingLaplace(StokesletWrapperBase, StokesletWrapperMixin): three-dimensional Stokes equations. Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim=None): + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to StokesletWrapper") self.kernel = LaplaceKernel(dim=self.dim) + self.mu = mu_sym + self.nu = nu_sym - def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): + def apply(self, density_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) @@ -531,7 +534,7 @@ def apply(self, density_vec_sym, mu_sym, qbx_forced_limit): qbx_forced_limit=qbx_forced_limit) sym_expr[i] += sym.S(AxisTargetDerivative(i, self.kernel), common_expr_density, qbx_forced_limit=qbx_forced_limit) - sym_expr[i] *= -0.5*(mu_sym**(-1)) + sym_expr[i] *= -0.5*(self.mu*(-1)) return sym_expr @@ -544,13 +547,15 @@ class StressletWrapperUsingLaplace(StokesletWrapperBase, StokesletWrapperMixin): three-dimensional Stokes equations. Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim=None): + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to StressletWrapper") self.kernel = LaplaceKernel(dim=self.dim) + self.mu = mu_sym + self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, mu_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) @@ -616,7 +621,7 @@ class StokesOperator: .. automethod:: pressure """ - def __init__(self, ambient_dim, side, method="naive"): + def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): """ :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. @@ -627,16 +632,20 @@ def __init__(self, ambient_dim, side, method="naive"): self.ambient_dim = ambient_dim self.side = side + self.mu = mu_sym + self.nu = nu_sym if method == "laplace": - self.stresslet = StressletWrapperUsingLaplace(dim=self.ambient_dim) - self.stokeslet = StokesletWrapperUsingLaplace(dim=self.ambient_dim) + self.stresslet = StressletWrapperUsingLaplace(dim=self.ambient_dim, + mu_sym=mu_sym, nu_sym=nu_sym) + self.stokeslet = StokesletWrapperUsingLaplace(dim=self.ambient_dim, + mu_sym=mu_sym, nu_sym=nu_sym) elif method == "biharmonic" or method == "naive": use_biharmonic = (method == "biharmonic") self.stresslet = StressletWrapper(dim=self.ambient_dim, - use_biharmonic=use_biharmonic) + use_biharmonic=use_biharmonic, mu_sym=mu_sym, nu_sym=nu_sym) self.stokeslet = StokesletWrapper(dim=self.ambient_dim, - use_biharmonic=use_biharmonic) + use_biharmonic=use_biharmonic, mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -651,7 +660,7 @@ def get_density_var(self, name="sigma"): """ return sym.make_sym_vector(name, self.ambient_dim) - def prepare_rhs(self, b, *, mu): + def prepare_rhs(self, b): """ :returns: a (potentially) modified right-hand side *b* that matches requirements of the representation. @@ -665,13 +674,13 @@ def operator(self, sigma): """ raise NotImplementedError - def velocity(self, sigma, *, normal, mu, qbx_forced_limit=None): + def velocity(self, sigma, *, normal, qbx_forced_limit=None): """ :returns: a representation of the velocity field in the Stokes flow. """ raise NotImplementedError - def pressure(self, sigma, *, normal, mu, qbx_forced_limit=None): + def pressure(self, sigma, *, normal, qbx_forced_limit=None): """ :returns: a representation of the pressure in the Stokes flow. """ @@ -695,7 +704,8 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=None, eta=None, method="naive"): + def __init__(self, *, omega, alpha=None, eta=None, method="naive", + mu_sym=var("mu"), nu_sym=0.5): r""" :arg omega: farfield behaviour of the velocity field, as defined by :math:`A` in [HsiaoKress1985]_ Equation 2.3. @@ -703,7 +713,8 @@ def __init__(self, *, omega, alpha=None, eta=None, method="naive"): :arg eta: real parameter :math:`\eta > 0`. Choosing this parameter well can have a non-trivial effect on the conditioning. """ - super().__init__(ambient_dim=2, side=+1, method=method) + super().__init__(ambient_dim=2, side=+1, method=method, + mu_sym=mu_sym, nu_sym=nu_sym) # NOTE: in [hsiao-kress], there is an analysis on a circle, which # recommends values in @@ -721,15 +732,14 @@ def __init__(self, *, omega, alpha=None, eta=None, method="naive"): self.alpha = alpha self.eta = eta - def _farfield(self, mu, qbx_forced_limit): + def _farfield(self, qbx_forced_limit): source_dofdesc = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) length = sym.integral(self.ambient_dim, self.dim, 1, dofdesc=source_dofdesc) return self.stokeslet.apply( -self.omega / length, - mu, qbx_forced_limit=qbx_forced_limit) - def _operator(self, sigma, normal, mu, qbx_forced_limit): + def _operator(self, sigma, normal, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit if slp_qbx_forced_limit == "avg": slp_qbx_forced_limit = "avg" @@ -743,32 +753,32 @@ def _operator(self, sigma, normal, mu, qbx_forced_limit): meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, self.dim, sigma, dofdesc=dd)) - op_k = self.stresslet.apply(sigma, normal, mu, - qbx_forced_limit=qbx_forced_limit) + op_k = self.stresslet.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit) op_s = ( self.alpha / (2.0 * np.pi) * int_sigma - - self.stokeslet.apply(meanless_sigma, mu, + - self.stokeslet.apply(meanless_sigma, qbx_forced_limit=slp_qbx_forced_limit) ) return op_k + self.eta * op_s - def prepare_rhs(self, b, *, mu): - return b + self._farfield(mu, qbx_forced_limit=+1) + def prepare_rhs(self, b): + return b + self._farfield(qbx_forced_limit=+1) - def operator(self, sigma, *, normal, mu): + def operator(self, sigma, *, normal): # NOTE: H. K. 1985 Equation 2.18 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator( - sigma, normal, mu, "avg")) + sigma, normal, "avg")) - def velocity(self, sigma, *, normal, mu, qbx_forced_limit=2): + def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 return merge_int_g_exprs( - -self._farfield(mu, qbx_forced_limit) - - self._operator(sigma, normal, mu, qbx_forced_limit) + -self._farfield(qbx_forced_limit) + - self._operator(sigma, normal, qbx_forced_limit) ) - def pressure(self, sigma, *, normal, mu, qbx_forced_limit=2): + def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: H. K. 1985 Equation 2.17 raise NotImplementedError @@ -786,13 +796,14 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None, method="naive"): + def __init__(self, *, eta=None, method="naive", mu_sym=var("mu"), nu_sym=0.5): r""" :arg eta: a parameter :math:`\eta > 0`. Choosing this parameter well can have a non-trivial effect on the conditioning of the operator. """ - super().__init__(ambient_dim=3, side=+1, method=method) + super().__init__(ambient_dim=3, side=+1, method=method, + mu_sym=mu_sym, nu_sym=nu_sym) # NOTE: eta is chosen here based on H. 1986 Figure 1, which is # based on solving on the unit sphere @@ -801,29 +812,28 @@ def __init__(self, *, eta=None, method="naive"): self.eta = eta - def _operator(self, sigma, normal, mu, qbx_forced_limit): + def _operator(self, sigma, normal, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit # if slp_qbx_forced_limit == "avg": # slp_qbx_forced_limit = self.side - op_w = self.stresslet.apply(sigma, normal, mu, + op_w = self.stresslet.apply(sigma, normal, qbx_forced_limit=qbx_forced_limit) - op_v = self.stokeslet.apply(sigma, mu, - qbx_forced_limit=slp_qbx_forced_limit) + op_v = self.stokeslet.apply(sigma, qbx_forced_limit=slp_qbx_forced_limit) return op_w + self.eta * op_v - def operator(self, sigma, *, normal, mu): + def operator(self, sigma, *, normal): # NOTE: H. 1986 Equation 17 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, - normal, mu, "avg")) + normal, "avg")) - def velocity(self, sigma, *, normal, mu, qbx_forced_limit=2): + def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 - return merge_int_g_exprs(-self._operator(sigma, normal, mu, + return merge_int_g_exprs(-self._operator(sigma, normal, qbx_forced_limit)) - def pressure(self, sigma, *, normal, mu, qbx_forced_limit=2): + def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the # equivalent single-/double-layer pressure kernels raise NotImplementedError diff --git a/test/test_stokes.py b/test/test_stokes.py index ea9ed0cbc..3cb0fa812 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -88,6 +88,7 @@ def run_exterior_stokes(ctx_factory, *, fine_order=source_ovsmp * target_order, qbx_order=qbx_order, fmm_order=fmm_order, + _max_leaf_refine_weight=64, target_association_tolerance=_target_association_tolerance, _expansions_in_tree_have_extent=_expansions_in_tree_have_extent) places["source"] = qbx @@ -136,25 +137,25 @@ def run_exterior_stokes(ctx_factory, *, if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator - sym_omega = sym.make_sym_vector("omega", ambient_dim) + sym_omega = sym.make_sym_vector("omega", ambient_dim, mu_sym=sym_mu) op = HsiaoKressExteriorStokesOperator(omega=sym_omega, method=method) elif ambient_dim == 3: from pytential.symbolic.stokes import HebekerExteriorStokesOperator - op = HebekerExteriorStokesOperator(method=method) + op = HebekerExteriorStokesOperator(method=method, mu_sym=sym_mu) else: assert False sym_sigma = op.get_density_var("sigma") sym_bc = op.get_density_var("bc") - sym_op = op.operator(sym_sigma, normal=sym_normal, mu=sym_mu) - sym_rhs = op.prepare_rhs(sym_bc, mu=mu) + sym_op = op.operator(sym_sigma, normal=sym_normal) + sym_rhs = op.prepare_rhs(sym_bc) - sym_velocity = op.velocity(sym_sigma, normal=sym_normal, mu=sym_mu) + sym_velocity = op.velocity(sym_sigma, normal=sym_normal) from pytential.symbolic.stokes import StokesletWrapper sym_source_pot = StokesletWrapper(ambient_dim, - use_biharmonic=False).apply(sym_sigma, sym_mu, qbx_forced_limit=None) + use_biharmonic=False).apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -282,7 +283,7 @@ def test_exterior_stokes(ctx_factory, ambient_dim, method="naive", visualize=Fal fmm_order = 10 resolutions = [20, 35, 50] elif ambient_dim == 3: - fmm_order = 8 + fmm_order = 6 resolutions = [0, 1, 2] else: raise ValueError(f"unsupported dimension: {ambient_dim}") @@ -298,8 +299,7 @@ def test_exterior_stokes(ctx_factory, ambient_dim, method="naive", visualize=Fal method=method) eoc.add_data_point(h_max, err) - - print(eoc) + print(eoc) # This convergence data is not as clean as it could be. See # https://github.com/inducer/pytential/pull/32 From 9207519b345eddbf270b5902035680d237fc9f52 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 10 May 2021 16:04:50 -0500 Subject: [PATCH 039/209] Support nu fully --- pytential/symbolic/stokes.py | 106 +++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index b6b80c5ca..204e58710 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -247,7 +247,7 @@ class StokesletWrapperMixin: """A base class for StokesletWrapper and StressletWrapper to create IntG instances """ - def get_int_g(self, idx, density, dir_vec_sym, qbx_forced_limit, + def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): """ @@ -256,39 +256,80 @@ def get_int_g(self, idx, density, dir_vec_sym, qbx_forced_limit, and its derivatives will be used instead of Stokeslet/Stresslet """ - def create_int_g(knl, deriv_dirs, **kwargs): + def create_int_g(knl, deriv_dirs, density, use_source_deriv=True, **kwargs): for deriv_dir in deriv_dirs: - knl = AxisSourceDerivative(deriv_dir, knl) + if use_source_deriv: + knl = AxisSourceDerivative(deriv_dir, knl) + else: + knl = AxisTargetDerivative(deriv_dir, knl) args = [arg.loopy_arg.name for arg in knl.get_args()] for arg in args: kwargs[arg] = var(arg) - res = sym.S(knl, density*dir_vec_sym[idx[-1]], + res = sym.S(knl, density, qbx_forced_limit=qbx_forced_limit, **kwargs) - return res*(-1)**len(deriv_dirs) + if use_source_deriv: + return res*(-1)**len(deriv_dirs) + else: + return res + + is_stresslet = (len(idx) == 3) + nu = self.nu if not self.use_biharmonic: knl = self.kernel_dict[idx] - return create_int_g(knl, deriv_dirs) - - deriv_relation = self.deriv_relation_dict[idx] - const = deriv_relation[0] - - # NOTE: we set a dofdesc here to force the evaluation of this integral - # on the source instead of the target when using automatic tagging - # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` - dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) - const *= sym.integral(self.dim, self.dim-1, density*dir_vec_sym[idx[-1]], - dofdesc=dd) - - result = const - for mi, coeff in deriv_relation[1]: - new_deriv_dirs = list(deriv_dirs) - for i, val in enumerate(mi): - new_deriv_dirs.extend([i]*val) - result += create_int_g(self.base_kernel, new_deriv_dirs) * coeff + result = create_int_g(knl, deriv_dirs, + density=density_sym*dir_vec_sym[idx[-1]], use_source_deriv=False) + if not is_stresslet: + return result + lknl = self.kernel_dict['laplace'] + lresult = 0 + lresult += create_int_g(lknl, deriv_dirs + [idx[0]], + density=density_sym*dir_vec_sym[idx[1]], use_source_deriv=False) + lresult -= create_int_g(lknl, deriv_dirs + [idx[1]], + density=density_sym*dir_vec_sym[idx[0]], use_source_deriv=False) + if idx[0] == idx[1]: + lresult -= create_int_g(lknl, deriv_dirs + [idx[2]], + density=density_sym*dir_vec_sym[idx[2]], use_source_deriv=False) + result = (result + lresult*(1 - 2*nu))/(2*(1 - nu)) + return result + + kernel_indices = [idx] + dir_vec_indices = [idx[-1]] + coeffs = [1] + extra_deriv_dirs_vec = [[]] + + if is_stresslet: + kernel_indices.extend(['laplace', 'laplace', 'laplace']) + dir_vec_indices.extend([idx[1], idx[0], idx[2]]) + coeffs.extend([1 - 2*nu, -(1 - 2*nu), -(1 - 2*nu)]) + extra_deriv_dirs_vec.extend([[idx[0]], [idx[1]], [idx[2]]]) + if idx[0] != idx[1]: + coeffs[-1] = 0 + + result = 0 + for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ + zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): + deriv_relation = self.deriv_relation_dict[kernel_idx] + const = deriv_relation[0] + + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) + const *= sym.integral(self.dim, self.dim-1, + density_sym*dir_vec_sym[dir_vec_idx], dofdesc=dd) + + if not extra_deriv_dirs: + result += const + for mi, c in deriv_relation[1]: + new_deriv_dirs = deriv_dirs + extra_deriv_dirs + for i, val in enumerate(mi): + new_deriv_dirs.extend([i]*val) + result += create_int_g(self.base_kernel, new_deriv_dirs, + density=density_sym*dir_vec_sym[dir_vec_idx]) * c * coeff return result @@ -308,7 +349,7 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): for i in range(dim): for j in range(i, dim): self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, - jcomp=j, viscosity_mu=mu_sym, poisson_ratio=nu_sym) + jcomp=j, viscosity_mu=str(mu_sym), poisson_ratio=str(nu_sym)) # The dictionary allows us to exploit symmetry -- that # :math:`T_{01}` is identical to :math:`T_{10}` -- and avoid creating @@ -322,7 +363,7 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): results = get_deriv_relation(list(self.kernel_dict.values()), self.base_kernel, tol=1e-10, order=2) self.deriv_relation_dict = {} - for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): + for deriv_eq, idx in zip(results, self.kernel_dict.keys()): self.deriv_relation_dict[idx] = deriv_eq def apply(self, density_vec_sym, qbx_forced_limit): @@ -432,10 +473,13 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): s = tuple(sorted([i, j, k])) self.kernel_dict[(i, j, k)] = self.kernel_dict[s] + # For elasticity (nu != 0.5), we need the LaplaceKernel + self.kernel_dict['laplace'] = LaplaceKernel(self.dim) + if self.use_biharmonic: from pytential.symbolic.pde.system_utils import get_deriv_relation results = get_deriv_relation(list(self.kernel_dict.values()), - self.base_kernel, tol=1e-10, order=3) + self.base_kernel, tol=1e-10, order=3, verbose=True) self.deriv_relation_dict = {} for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): self.deriv_relation_dict[idx] = deriv_eq @@ -448,7 +492,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i, j), - dir_vec_sym[i], density_vec_sym, + density_vec_sym[i], dir_vec_sym, qbx_forced_limit, deriv_dirs=[]) return sym_expr @@ -462,7 +506,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i, j), - dir_vec_sym[i], density_vec_sym, + density_vec_sym[i], dir_vec_sym, qbx_forced_limit, deriv_dirs=[deriv_dir]) return sym_expr @@ -643,9 +687,11 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): elif method == "biharmonic" or method == "naive": use_biharmonic = (method == "biharmonic") self.stresslet = StressletWrapper(dim=self.ambient_dim, - use_biharmonic=use_biharmonic, mu_sym=mu_sym, nu_sym=nu_sym) + use_biharmonic=use_biharmonic, + mu_sym=mu_sym, nu_sym=nu_sym) self.stokeslet = StokesletWrapper(dim=self.ambient_dim, - use_biharmonic=use_biharmonic, mu_sym=mu_sym, nu_sym=nu_sym) + use_biharmonic=use_biharmonic, + mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") From bfeb4f13896825441b4f02da8a9144cbd6e33d70 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 10 May 2021 16:47:10 -0500 Subject: [PATCH 040/209] Support Quotient in merge_int_g_exprs --- pytential/symbolic/pde/system_utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 0f4ec8ae3..36903a79c 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -30,7 +30,7 @@ as gnitstam) from pymbolic.mapper import CombineMapper -from pymbolic.primitives import Sum, Product +from pymbolic.primitives import Sum, Product, Quotient from pytential.symbolic.primitives import IntG @@ -267,6 +267,12 @@ def _merge_int_g_expr(expr): return coeff*mult, new_int_g.copy(densities=new_densities) elif isinstance(expr, IntG): return 0, _convert_target_deriv_to_source(expr) + elif isinstance(expr, Quotient): + mult = 1/expr.denominator + coeff, new_int_g = _merge_int_g_expr(expr.numerator) + new_densities = (density * mult for \ + density in new_int_g.densities) + return coeff*mult, new_int_g.copy(densities=new_densities) else: return expr, 0 @@ -278,5 +284,5 @@ def _merge_int_g_expr(expr): kernels = [StokesletKernel(3, 0, 1), StokesletKernel(3, 0, 0)] kernels = [StressletKernel(3, 0, 1, 0), StressletKernel(3, 0, 0, 0), StressletKernel(3, 0, 1, 2)] - kernels = [ElasticityKernel(3, 0, 1), ElasticityKernel(3, 0, 0)] + kernels = [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] get_deriv_relation(kernels, base_kernel, tol=1e-10, order=2, verbose=True) From 1f1f7c612679e6d0711c00c88024ce14e9b3fcb7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 10 May 2021 16:47:39 -0500 Subject: [PATCH 041/209] refactor and fix missing division by 2*(1-nu) --- pytential/symbolic/stokes.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 204e58710..a4e5559ac 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -277,25 +277,6 @@ def create_int_g(knl, deriv_dirs, density, use_source_deriv=True, **kwargs): is_stresslet = (len(idx) == 3) nu = self.nu - - if not self.use_biharmonic: - knl = self.kernel_dict[idx] - result = create_int_g(knl, deriv_dirs, - density=density_sym*dir_vec_sym[idx[-1]], use_source_deriv=False) - if not is_stresslet: - return result - lknl = self.kernel_dict['laplace'] - lresult = 0 - lresult += create_int_g(lknl, deriv_dirs + [idx[0]], - density=density_sym*dir_vec_sym[idx[1]], use_source_deriv=False) - lresult -= create_int_g(lknl, deriv_dirs + [idx[1]], - density=density_sym*dir_vec_sym[idx[0]], use_source_deriv=False) - if idx[0] == idx[1]: - lresult -= create_int_g(lknl, deriv_dirs + [idx[2]], - density=density_sym*dir_vec_sym[idx[2]], use_source_deriv=False) - result = (result + lresult*(1 - 2*nu))/(2*(1 - nu)) - return result - kernel_indices = [idx] dir_vec_indices = [idx[-1]] coeffs = [1] @@ -309,6 +290,17 @@ def create_int_g(knl, deriv_dirs, density, use_source_deriv=True, **kwargs): if idx[0] != idx[1]: coeffs[-1] = 0 + if not self.use_biharmonic: + result = 0 + for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ + zip(kernel_indices, dir_vec_indices, coeffs, + extra_deriv_dirs_vec): + knl = self.kernel_dict[idx] + result += create_int_g(knl, deriv_dirs + extra_deriv_dirs, + density=density_sym*dir_vec_sym[dir_vec_idx], + use_source_deriv=False) * coeff + return result/(2*(1 - nu)) + result = 0 for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): @@ -331,7 +323,7 @@ def create_int_g(knl, deriv_dirs, density, use_source_deriv=True, **kwargs): result += create_int_g(self.base_kernel, new_deriv_dirs, density=density_sym*dir_vec_sym[dir_vec_idx]) * c * coeff - return result + return result/(2*(1 - nu)) class StokesletWrapper(StokesletWrapperBase, StokesletWrapperMixin): @@ -479,7 +471,7 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): if self.use_biharmonic: from pytential.symbolic.pde.system_utils import get_deriv_relation results = get_deriv_relation(list(self.kernel_dict.values()), - self.base_kernel, tol=1e-10, order=3, verbose=True) + self.base_kernel, tol=1e-10, order=3, verbose=False) self.deriv_relation_dict = {} for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): self.deriv_relation_dict[idx] = deriv_eq From cb5b6927e4a14a40ed5282b683754cd150c7dc89 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 20 May 2021 00:40:20 -0500 Subject: [PATCH 042/209] expose laplace int g --- pytential/symbolic/stokes.py | 111 ++++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index a4e5559ac..c531bc100 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -247,6 +247,51 @@ class StokesletWrapperMixin: """A base class for StokesletWrapper and StressletWrapper to create IntG instances """ + def _create_int_g(self, knl, deriv_dirs, density, use_source_deriv=True, + **kwargs): + for deriv_dir in deriv_dirs: + if use_source_deriv: + knl = AxisSourceDerivative(deriv_dir, knl) + else: + knl = AxisTargetDerivative(deriv_dir, knl) + + args = [arg.loopy_arg.name for arg in knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) + + res = sym.S(knl, density, **kwargs) + + if use_source_deriv: + return res*(-1)**len(deriv_dirs) + else: + return res + + def _get_int_g_deriv_relation(self, kernel_idx, density, deriv_dirs, coeff, + qbx_forced_limit): + deriv_relation = self.deriv_relation_dict[kernel_idx] + const = deriv_relation[0] + + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) + const *= sym.integral(self.dim, self.dim-1, density, dofdesc=dd) + + result = 0 + if not deriv_dirs: + result += const + for mi, c in deriv_relation[1]: + new_deriv_dirs = deriv_dirs.copy() + for i, val in enumerate(mi): + new_deriv_dirs.extend([i]*val) + result += self._create_int_g(self.base_kernel, new_deriv_dirs, + density=density, qbx_forced_limit=qbx_forced_limit) * c * coeff + return result + + def get_laplace_int_g(self, density, qbx_forced_limit, deriv_dirs): + return self._get_int_g_deriv_relation('laplace', density, deriv_dirs, 1, + qbx_forced_limit) + def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): @@ -256,25 +301,6 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, and its derivatives will be used instead of Stokeslet/Stresslet """ - def create_int_g(knl, deriv_dirs, density, use_source_deriv=True, **kwargs): - for deriv_dir in deriv_dirs: - if use_source_deriv: - knl = AxisSourceDerivative(deriv_dir, knl) - else: - knl = AxisTargetDerivative(deriv_dir, knl) - - args = [arg.loopy_arg.name for arg in knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) - - res = sym.S(knl, density, - qbx_forced_limit=qbx_forced_limit, **kwargs) - - if use_source_deriv: - return res*(-1)**len(deriv_dirs) - else: - return res - is_stresslet = (len(idx) == 3) nu = self.nu kernel_indices = [idx] @@ -296,32 +322,19 @@ def create_int_g(knl, deriv_dirs, density, use_source_deriv=True, **kwargs): zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): knl = self.kernel_dict[idx] - result += create_int_g(knl, deriv_dirs + extra_deriv_dirs, + result += self._create_int_g(knl, deriv_dirs + extra_deriv_dirs, density=density_sym*dir_vec_sym[dir_vec_idx], - use_source_deriv=False) * coeff + use_source_deriv=False, + qbx_forced_limit=qbx_forced_limit) * coeff return result/(2*(1 - nu)) result = 0 for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): - deriv_relation = self.deriv_relation_dict[kernel_idx] - const = deriv_relation[0] - - # NOTE: we set a dofdesc here to force the evaluation of this integral - # on the source instead of the target when using automatic tagging - # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` - dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) - const *= sym.integral(self.dim, self.dim-1, - density_sym*dir_vec_sym[dir_vec_idx], dofdesc=dd) - - if not extra_deriv_dirs: - result += const - for mi, c in deriv_relation[1]: - new_deriv_dirs = deriv_dirs + extra_deriv_dirs - for i, val in enumerate(mi): - new_deriv_dirs.extend([i]*val) - result += create_int_g(self.base_kernel, new_deriv_dirs, - density=density_sym*dir_vec_sym[dir_vec_idx]) * c * coeff + result += self._get_int_g_deriv_relation(kernel_idx, + density_sym*dir_vec_sym[dir_vec_idx], + deriv_dirs + extra_deriv_dirs, coeff, + qbx_forced_limit=qbx_forced_limit) return result/(2*(1 - nu)) @@ -341,7 +354,7 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): for i in range(dim): for j in range(i, dim): self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, - jcomp=j, viscosity_mu=str(mu_sym), poisson_ratio=str(nu_sym)) + jcomp=j, viscosity_mu=mu_sym, poisson_ratio=nu_sym) # The dictionary allows us to exploit symmetry -- that # :math:`T_{01}` is identical to :math:`T_{10}` -- and avoid creating @@ -536,7 +549,7 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # {{{ Stokeslet/Stresslet using Laplace -class StokesletWrapperUsingLaplace(StokesletWrapperBase, StokesletWrapperMixin): +class StokesletWrapperUsingLaplace(StokesletWrapperBase): """Stokeslet Wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -574,8 +587,14 @@ def apply(self, density_vec_sym, qbx_forced_limit): return sym_expr + def get_laplace_int_g(self, density, qbx_forced_limit, deriv_dirs): + knl = self.kernel + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + return sym.S(knl, density, qbx_forced_limit=qbx_forced_limit) + -class StressletWrapperUsingLaplace(StokesletWrapperBase, StokesletWrapperMixin): +class StressletWrapperUsingLaplace(StokesletWrapperBase): """Stresslet Wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -804,10 +823,10 @@ def _operator(self, sigma, normal, qbx_forced_limit): def prepare_rhs(self, b): return b + self._farfield(qbx_forced_limit=+1) - def operator(self, sigma, *, normal): + def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. K. 1985 Equation 2.18 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator( - sigma, normal, "avg")) + sigma, normal, qbx_forced_limit)) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 @@ -861,10 +880,10 @@ def _operator(self, sigma, normal, qbx_forced_limit): return op_w + self.eta * op_v - def operator(self, sigma, *, normal): + def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, - normal, "avg")) + normal, qbx_forced_limit)) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 From 5b434b2be16e4aa09fe896bfe12017300ddba02a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 21 May 2021 10:24:48 -0500 Subject: [PATCH 043/209] automatically convert IntGs to base kernel if given --- pytential/symbolic/pde/system_utils.py | 334 ++++++++++++++++++++----- 1 file changed, 268 insertions(+), 66 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 36903a79c..086a4e7d2 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -24,21 +24,23 @@ from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, - DirectionalSourceDerivative) -from pytools import ( + DirectionalSourceDerivative, ExpressionKernel, + KernelWrapper, TargetPointMultiplier) +from pytools import (memoize_on_first_arg, generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) -from pymbolic.mapper import CombineMapper +from pymbolic.mapper import WalkMapper from pymbolic.primitives import Sum, Product, Quotient -from pytential.symbolic.primitives import IntG +from pytential.symbolic.primitives import IntG, NodeCoordinateComponent, int_g_vec +import pytential def _chop(expr, tol): nums = expr.atoms(sym.Number) replace_dict = {} for num in nums: - if abs(num) < tol: + if float(abs(num)) < tol: replace_dict[num] = 0 else: new_num = float(num) @@ -58,19 +60,33 @@ def _n(expr): return expr.n(n=30) -def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): +@memoize_on_first_arg +def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): dim = base_kernel.dim - mis = sorted(gnitstam(order, dim), key=sum) + pde = base_kernel.get_pde_as_diff_op() + if order is None: + order = pde.degree + + if order > pde.degree: + raise NotImplementedError(f"order ({order}) cannot be greater than the order" + f"of the PDE ({pde.degree}) yet.") + mis = sorted(gnitstam(order, dim), key=sum) # (-1, -1, -1) represent a constant mis.append((-1, -1, -1)) + if order == pde.degree: + pde_mis = [ident.mi for eq in pde.eqs for ident in eq.keys()] + pde_mis = [mi for mi in pde_mis if sum(mi) == order] + if verbose: + print(f"Removing {pde_mis[-1]} to avoid linear dependent mis") + mis.remove(pde_mis[-1]) + rand = np.random.randint(1, 100, (dim, len(mis))) sym_vec = make_sym_vector("d", dim) base_expr = base_kernel.get_expression(sym_vec) - sympy_conv = SympyToPymbolicMapper() mat = [] for rand_vec_idx in range(rand.shape[1]): @@ -90,70 +106,228 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=False): mat.append(row) mat = sym.Matrix(mat) - res = [] + L, U, perm = mat.LUdecomposition() + return (L, U, perm), rand, mis + + +def _LUsolve_with_expand(L, U, perm, b): + def forward_substitution(L, b): + n = len(b) + res = sym.Matrix(b) + for i in range(n): + for j in range(i): + res[i] -= L[i, j]*res[j] + res[i] = (res[i] / L[i, i]).expand() + return res + + def backward_substitution(U, b): + n = len(b) + res = sym.Matrix(b) + for i in range(n-1, -1, -1): + for j in range(n - 1, i, -1): + res[i] -= U[i, j]*res[j] + res[i] = (res[i] / U[i, i]).expand() + return res + + def permuteFwd(b, perm): + res = sym.Matrix(b) + for p, q in perm: + res[p], res[q] = res[q], res[p] + return res + + return backward_substitution(U, + forward_substitution(L, permuteFwd(b, perm))) + + +@memoize_on_first_arg +def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, + verbose=False): + (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, + verbose=verbose) + dim = base_kernel.dim + sym_vec = make_sym_vector("d", dim) + sympy_conv = SympyToPymbolicMapper() + + expr = kernel.get_expression(sym_vec) + vec = [] + for i in range(len(mis)): + vec.append(_n(expr.xreplace(dict((k, v) for + k, v in zip(sym_vec, rand[:, i]))))) + vec = sym.Matrix(vec) + result = [] + const = 0 + if verbose: + print(kernel, end=" = ", flush=True) + + sol = _LUsolve_with_expand(L, U, perm, vec) + for i, coeff in enumerate(sol): + coeff = _chop(coeff, tol) + if coeff == 0: + continue + if mis[i] != (-1, -1, -1): + coeff *= kernel.get_global_scaling_const() + coeff /= base_kernel.get_global_scaling_const() + result.append((mis[i], sympy_conv(coeff))) + if verbose: + print(f"{base_kernel}.diff({mis[i]})*{coeff}", end=" + ") + else: + const = sympy_conv(coeff * kernel.get_global_scaling_const()) + if verbose: + print(const) + return (const, result) - for kernel in kernels: - expr = kernel.get_expression(sym_vec) - vec = [] - for i in range(len(mis)): - vec.append(_n(expr.xreplace(dict((k, v) for - k, v in zip(sym_vec, rand[:, i]))))) - vec = sym.Matrix(vec) - result = [] - const = 0 - if verbose: - print(kernel, end=" = ", flush=True) - for i, coeff in enumerate(mat.solve(vec)): - coeff = _chop(coeff.expand(), tol) - if coeff == 0: - continue - if mis[i] != (-1, -1, -1): - coeff *= kernel.get_global_scaling_const() - coeff /= base_kernel.get_global_scaling_const() - result.append((mis[i], sympy_conv(coeff))) - if verbose: - print(f"{base_kernel}.diff({mis[i]})*{coeff}", end=" + ") - else: - const = sympy_conv(coeff * kernel.get_global_scaling_const()) - if verbose: - print(const) - res.append((const, result)) +def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, verbose=False): + res = [] + for knl in kernels: + res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order, verbose)) return res -class HaveIntGs(CombineMapper): - def combine(self, values): - return sum(values) +class GetIntGs(WalkMapper): + def __init__(self): + self.int_g_s = set() def map_int_g(self, expr): - return 1 + self.int_g_s.add(expr) def map_constant(self, expr): - return 0 + pass + + map_variable = map_constant + handle_unsupported_expression = map_constant - def map_variable(self, expr): - return 0 - def handle_unsupported_expression(self, expr, *args, **kwargs): - return 0 +def have_int_g_s(expr): + mapper = GetIntGs() + mapper(expr) + return bool(mapper.int_g_s) -def merge_int_g_exprs(exprs): - return np.array([merge_int_g_expr(expr) for expr in exprs]) +def convert_int_g_to_base(int_g, base_kernel, verbose=False): + result = 0 + for knl, density in zip(int_g.source_kernels, int_g.densities): + result += _convert_int_g_to_base( + int_g.copy(source_kernels=(knl,), densities=(density,)), + base_kernel, verbose) + return result + + +def _convert_int_g_to_base(int_g, base_kernel, verbose=False): + tgt_knl = int_g.target_kernel + dim = tgt_knl.dim + if tgt_knl != tgt_knl.get_base_kernel(): + return int_g + + assert len(int_g.densities) == 1 + density = int_g.densities[0] + source_kernel = int_g.source_kernels[0] + + deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), + base_kernel, verbose=verbose) + + const = deriv_relation[0] + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = pytential.sym.DOFDescriptor(None, + discr_stage=pytential.sym.QBX_SOURCE_STAGE1) + const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) + + result = 0 + if source_kernel == source_kernel.get_base_kernel(): + result += const + + for mi, c in deriv_relation[1]: + knl = source_kernel.replace_base_kernel(base_kernel) + for d, val in enumerate(mi): + for _ in range(val): + knl = AxisSourceDerivative(d, knl) + c *= -1 + result += int_g.copy(target_kernel=base_kernel, source_kernels=(knl,), + densities=(density,)) * c + return result + + +def merge_int_g_exprs(exprs, base_kernel=None, verbose=False): + replacements = {} + + if base_kernel is not None: + mapper = GetIntGs() + [mapper(expr) for expr in exprs] + int_g_s = mapper.int_g_s + for int_g in int_g_s: + new_int_g = _convert_target_deriv_to_source(int_g) + tgt_knl = new_int_g.target_kernel + if isinstance(tgt_knl, TargetPointMultiplier) \ + and not isinstance(tgt_knl.inner_kernel, KernelWrapper): + new_int_g_s = _convert_target_multiplier_to_source(new_int_g) + else: + new_int_g_s = [new_int_g] + replacements[int_g] = sum(convert_int_g_to_base(new_int_g, + base_kernel, verbose=verbose) for new_int_g in new_int_g_s) + + return np.array([merge_int_g_expr(expr, replacements) for expr in exprs]) + + +def _convert_target_multiplier_to_source(int_g): + from sumpy.symbolic import SympyToPymbolicMapper + tgt_knl = int_g.target_kernel + if not isinstance(tgt_knl, TargetPointMultiplier): + return int_g + if isinstance(tgt_knl.inner_kernel, KernelWrapper): + return int_g + result = [] + # x G = y*G + (x - y)*G + # For y*G, absorb y into a density + new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) + for density in int_g.densities] + result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, + densities=tuple(new_densities))) + + # create a new expression kernel for (x - y)*G + sym_d = make_sym_vector("d", tgt_knl.dim) + conv = SympyToPymbolicMapper() + + for knl, density in zip(int_g.source_kernels, int_g.densities): + new_expr = conv(knl.postprocess_at_source(knl.get_expression(sym_d), sym_d) + * sym_d[tgt_knl.axis]) + new_knl = ExpressionKernel(knl.dim, new_expr, + knl.get_base_kernel().global_scaling_const, + knl.is_complex_valued) + result.append(int_g.copy(target_kernel=new_knl, + densities=(density,), + source_kernels=(new_knl,))) + return result def _convert_target_deriv_to_source(int_g): knl = int_g.target_kernel source_kernels = list(int_g.source_kernels) coeff = 1 + multipliers = [] + while isinstance(knl, TargetPointMultiplier): + multipliers.append(knl.axis) + knl = knl.inner_kernel + while isinstance(knl, AxisTargetDerivative): - coeff *= 1 + coeff *= -1 source_kernels = [AxisSourceDerivative(knl.axis, source_knl) for source_knl in source_kernels] knl = knl.inner_kernel - return coeff * int_g.copy(target_kernel=knl, - source_kernels=tuple(source_kernels)) + + # TargetPointMultiplier has to be the outermost kernel + # If it is the inner kernel, return early + if isinstance(knl, TargetPointMultiplier): + return 1, int_g + + for axis in reversed(multipliers): + knl = TargetPointMultiplier(axis, knl) + + new_densities = tuple(density*coeff for density in int_g.densities) + return int_g.copy(target_kernel=knl, + densities=new_densities, + source_kernels=tuple(source_kernels)) def _convert_axis_source_to_directional_source(int_g): @@ -185,13 +359,14 @@ def _convert_axis_source_to_directional_source(int_g): return res -def merge_int_g_expr(expr): - have_int_g = HaveIntGs() - if have_int_g(expr) <= 1: +def merge_int_g_expr(expr, replacements): + if not have_int_g_s(expr): return expr try: - result_coeff, result_int_g = _merge_int_g_expr(expr) + result_coeff, result_int_g = _merge_int_g_expr(expr, replacements) result_int_g = _convert_axis_source_to_directional_source(result_int_g) + result_int_g = result_int_g.copy( + densities=_simplify_densities(result_int_g.densities)) return result_coeff + result_int_g except AssertionError: return expr @@ -220,12 +395,26 @@ def _merge_kernel_arguments(x, y): return res -def _merge_int_g_expr(expr): +def _simplify_densities(densities): + from sumpy.symbolic import (SympyToPymbolicMapper, PymbolicToSympyMapper) + from pymbolic.mapper import UnsupportedExpressionError + to_sympy = PymbolicToSympyMapper() + to_pymbolic = SympyToPymbolicMapper() + result = [] + for density in densities: + try: + result.append(to_pymbolic(to_sympy(density))) + except (ValueError, NotImplementedError, UnsupportedExpressionError): + result.append(density) + return tuple(result) + + +def _merge_int_g_expr(expr, replacements): if isinstance(expr, Sum): result_coeff = 0 result_int_g = 0 for c in expr.children: - coeff, int_g = _merge_int_g_expr(c) + coeff, int_g = _merge_int_g_expr(c, replacements) result_coeff += coeff if int_g == 0: continue @@ -251,9 +440,8 @@ def _merge_int_g_expr(expr): elif isinstance(expr, Product): mult = 1 found_int_g = None - have_int_g = HaveIntGs() for c in expr.children: - if not have_int_g(c): + if not have_int_g_s(c): mult *= c elif found_int_g: raise RuntimeError("Not a linear expression.") @@ -262,27 +450,41 @@ def _merge_int_g_expr(expr): if not found_int_g: return expr, 0 else: - coeff, new_int_g = _merge_int_g_expr(found_int_g) + coeff, new_int_g = _merge_int_g_expr(found_int_g, replacements) new_densities = (density * mult for density in new_int_g.densities) return coeff*mult, new_int_g.copy(densities=new_densities) elif isinstance(expr, IntG): - return 0, _convert_target_deriv_to_source(expr) + new_expr = replacements.get(expr, expr) + if new_expr == expr: + new_int_g = _convert_target_deriv_to_source(expr) + return 0, new_int_g + else: + return _merge_int_g_expr(new_expr, replacements) elif isinstance(expr, Quotient): mult = 1/expr.denominator - coeff, new_int_g = _merge_int_g_expr(expr.numerator) - new_densities = (density * mult for \ - density in new_int_g.densities) - return coeff*mult, new_int_g.copy(densities=new_densities) + coeff, new_int_g = _merge_int_g_expr(expr.numerator, replacements) + new_densities = (density * mult for density in new_int_g.densities) + return coeff * mult, new_int_g.copy(densities=new_densities) else: return expr, 0 if __name__ == "__main__": from sumpy.kernel import (StokesletKernel, BiharmonicKernel, StressletKernel, - ElasticityKernel) + ElasticityKernel, LaplaceKernel) base_kernel = BiharmonicKernel(3) kernels = [StokesletKernel(3, 0, 1), StokesletKernel(3, 0, 0)] - kernels = [StressletKernel(3, 0, 1, 0), StressletKernel(3, 0, 0, 0), + kernels += [StressletKernel(3, 0, 1, 0), StressletKernel(3, 0, 0, 0), StressletKernel(3, 0, 1, 2)] - kernels = [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] + kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), + ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] get_deriv_relation(kernels, base_kernel, tol=1e-10, order=2, verbose=True) + density = pytential.sym.make_sym_vector("d", 1)[0] + int_g_1 = int_g_vec(TargetPointMultiplier(2, AxisTargetDerivative(2, + AxisSourceDerivative(1, AxisSourceDerivative(0, + LaplaceKernel(3))))), density, qbx_forced_limit=1) + int_g_2 = int_g_vec(TargetPointMultiplier(0, AxisTargetDerivative(0, + AxisSourceDerivative(0, AxisSourceDerivative(0, + LaplaceKernel(3))))), density, qbx_forced_limit=1) + print(merge_int_g_exprs([int_g_1, int_g_2], + base_kernel=BiharmonicKernel(3), verbose=True)[0]) From cd8195bab47c5fd3751d9c9f5ddbe17f3465dd67 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 22 May 2021 00:25:50 -0500 Subject: [PATCH 044/209] Add MindlinOperator --- pytential/symbolic/elasticity.py | 189 +++++++++++++++++++++++++++++ pytential/symbolic/stokes.py | 201 +++++++++++-------------------- 2 files changed, 256 insertions(+), 134 deletions(-) create mode 100644 pytential/symbolic/elasticity.py diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py new file mode 100644 index 000000000..2cc5fe375 --- /dev/null +++ b/pytential/symbolic/elasticity.py @@ -0,0 +1,189 @@ +__copyright__ = "Copyright (C) 2021 Isuru Fernando" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import numpy as np + +from pytential import sym +from pytential.symbolic.pde.system_utils import merge_int_g_exprs +from sumpy.kernel import (LineOfCompressionKernel, + AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) +from pymbolic import var +from pytential.symbolic.stokes import HebekerExteriorStokesOperator, StokesOperator +from pytential.symbolic.primitives import NodeCoordinateComponent + + +class MindlinOperator(StokesOperator): + """Representation for 3D Stokes Flow based on [Hebeker1986]_. + + Inherits from :class:`StokesOperator`. + + .. [Hebeker1986] F. C. Hebeker, *Efficient Boundary Element Methods for + Three-Dimensional Exterior Viscous Flow*, Numerical Methods for + Partial Differential Equations, Vol. 2, 1986, + `DOI `__. + + .. automethod:: __init__ + """ + + def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu")): + if method not in ["biharmonic", "laplace"]: + raise ValueError(f"invalid method: {method}." + "Needs to be one of laplace, biharmonic") + + self.method = method + self.mu = mu_sym + self.nu = nu_sym + self._stokes = HebekerExteriorStokesOperator(eta=0, method=method, + mu_sym=mu_sym, nu_sym=nu_sym) + self._stokes_a = HebekerExteriorStokesOperator(eta=0, method=method, + mu_sym=mu_sym + 4*nu_sym, nu_sym=-nu_sym) + self.dim = 3 + self.compression_knl = LineOfCompressionKernel(self.dim, 2, mu_sym, nu_sym) + + def K(self, sigma, normal, qbx_forced_limit): + return merge_int_g_exprs(self._stokes.stresslet.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit)) + + def A(self, sigma, normal, qbx_forced_limit): + result = -self._stokes_a.stresslet.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit) + new_density = sum(a*b for a, b in zip(sigma, normal)) + for i in range(self.dim): + result[i] += 2*self._create_int_g(self._stokes.laplace_kernel, [], [i], + new_density, qbx_forced_limit) + return result + + def B(self, sigma, normal, qbx_forced_limit): + + def create_int_g(density, source_dirs, target_dir): + knl = self.compression_knl + for source_dir in source_dirs: + knl = AxisSourceDerivative(source_dir, knl) + knl = AxisTargetDerivative(target_dir, knl) + kwargs = {} + args = [arg.loopy_arg.name for arg in knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) + + return sym.int_g_vec(knl, density, qbx_forced_limit=qbx_forced_limit, + **kwargs) + + sym_expr = np.zeros((self.dim,), dtype=object) + mu = self.mu + nu = self.nu + lam = 2*nu*mu/(1-2*nu) + sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) + + for i in range(self.dim): + val = self._create_int_g(sigma[0]*normal[0], [0, 0], [i]) + val += self._create_int_g(sigma[1]*normal[1], [1, 1], [i]) + val -= self._create_int_g(sigma[2]*normal[2], [2, 2], [i]) + val += self._create_int_g(sigma[0]*normal[1]+sigma[1]*normal[0], + [0, 1], [i]) + val *= 2*mu + val -= 2*lam*create_int_g(sigma_normal_product, [2, 2], i) + if i == 2: + val *= -1 + sym_expr[i] = val + + return sym_expr + + def C(self, sigma, normal, qbx_forced_limit): + result = np.zeros((self.dim,), dtype=object) + mu = self.mu + nu = self.nu + lam = 2*nu*mu/(1-2*nu) + alpha = (lam + mu)/(lam + 2*mu) + y = [NodeCoordinateComponent(i) for i in range(self.dim)] + sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) + + def get_laplace_int_g(density, source_deriv_dirs, target_deriv_dirs=[]): + int_g = self._create_int_g(self._stokes.laplace_kernel, + source_deriv_dirs, target_deriv_dirs, density, + qbx_forced_limit=qbx_forced_limit) + return int_g.copy( + target_kernel=TargetPointMultiplier(2, int_g.target_kernel)) + + for i in range(self.dim): + # phi_c in Gimbutas et, al. + temp = get_laplace_int_g(y[2]*sigma[0]*normal[0], [0, 0], [i]) + temp += get_laplace_int_g(y[2]*sigma[1]*normal[1], [1, 1], [i]) + temp += get_laplace_int_g(y[2]*sigma[2]*normal[2], [2, 2], [i]) + temp += get_laplace_int_g(y[2]*(sigma[0]*normal[1] + sigma[1]*normal[0]), + [0, 1], [i]) + temp -= get_laplace_int_g(y[2]*(sigma[0]*normal[2] + sigma[2]*normal[0]), + [0, 2], [i]) + temp -= get_laplace_int_g(y[2]*(sigma[1]*normal[2] + sigma[2]*normal[1]), + [1, 2], [i]) + temp *= -2*alpha*mu + result[i] += temp + + # G in Gimbutas et, al. + temp = get_laplace_int_g( + y[2]*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), [0], [i]) + temp += get_laplace_int_g( + y[2]*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), [1], [i]) + temp += get_laplace_int_g( + y[2]*(mu*-2*sigma[2]*normal[2] - lam*sigma_normal_product), [2], [i]) + temp *= (2*alpha - 2) + result[i] += temp + + # H in Gimubtas et, al. + temp = get_laplace_int_g(sigma[0]*normal[0], [0, 0]) + temp += get_laplace_int_g(sigma[1]*normal[1], [1, 1]) + temp += get_laplace_int_g(sigma[2]*normal[2], [2, 2]) + temp += get_laplace_int_g(sigma[0]*normal[1] + sigma[1]*normal[0], [0, 1]) + temp -= get_laplace_int_g(sigma[0]*normal[2] + sigma[2]*normal[0], [0, 2]) + temp -= get_laplace_int_g(sigma[1]*normal[2] + sigma[2]*normal[1], [1, 2]) + + result[2] -= -2*(2 - alpha)*mu*temp + + return result + + def operator(self, sigma, *, normal, qbx_forced_limit="avg"): + # A and C are both derivatives of Biharmonic Green's function + # TODO: make merge_int_g_exprs smart enough to merge two different + # kernels into two separate IntGs. + result = self.A(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + result += self.C(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + result = merge_int_g_exprs(result, base_kernel=self._stokes.base_kernel) + + result += merge_int_g_exprs(self.B(sigma, normal=normal, + qbx_forced_limit=qbx_forced_limit), base_kernel=self.compression_knl) + return result + + def _create_int_g(self, knl, source_deriv_dirs, target_deriv_dirs, density, + **kwargs): + for deriv_dir in source_deriv_dirs: + knl = AxisSourceDerivative(deriv_dir, knl) + + for deriv_dir in target_deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + if deriv_dir == 2: + density *= -1 + + args = [arg.loopy_arg.name for arg in knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) + + res = sym.S(knl, density, **kwargs) + return res diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index c531bc100..94105dde3 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -243,114 +243,26 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, raise NotImplementedError -class StokesletWrapperMixin: - """A base class for StokesletWrapper and StressletWrapper - to create IntG instances - """ - def _create_int_g(self, knl, deriv_dirs, density, use_source_deriv=True, - **kwargs): - for deriv_dir in deriv_dirs: - if use_source_deriv: - knl = AxisSourceDerivative(deriv_dir, knl) - else: - knl = AxisTargetDerivative(deriv_dir, knl) - - args = [arg.loopy_arg.name for arg in knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) - - res = sym.S(knl, density, **kwargs) - - if use_source_deriv: - return res*(-1)**len(deriv_dirs) - else: - return res - - def _get_int_g_deriv_relation(self, kernel_idx, density, deriv_dirs, coeff, - qbx_forced_limit): - deriv_relation = self.deriv_relation_dict[kernel_idx] - const = deriv_relation[0] - - # NOTE: we set a dofdesc here to force the evaluation of this integral - # on the source instead of the target when using automatic tagging - # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` - dd = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) - const *= sym.integral(self.dim, self.dim-1, density, dofdesc=dd) +def _create_int_g(knl, deriv_dirs, density, **kwargs): + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) - result = 0 - if not deriv_dirs: - result += const - for mi, c in deriv_relation[1]: - new_deriv_dirs = deriv_dirs.copy() - for i, val in enumerate(mi): - new_deriv_dirs.extend([i]*val) - result += self._create_int_g(self.base_kernel, new_deriv_dirs, - density=density, qbx_forced_limit=qbx_forced_limit) * c * coeff - return result - - def get_laplace_int_g(self, density, qbx_forced_limit, deriv_dirs): - return self._get_int_g_deriv_relation('laplace', density, deriv_dirs, 1, - qbx_forced_limit) + args = [arg.loopy_arg.name for arg in knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) - def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, - deriv_dirs): + res = sym.S(knl, density, **kwargs) + return res - """ - Returns the Integral of the Stokeslet/Stresslet kernel given by `idx` - and its derivatives. If `use_biharmonic` is set, Biharmonic Kernel - and its derivatives will be used instead of Stokeslet/Stresslet - """ - is_stresslet = (len(idx) == 3) - nu = self.nu - kernel_indices = [idx] - dir_vec_indices = [idx[-1]] - coeffs = [1] - extra_deriv_dirs_vec = [[]] - - if is_stresslet: - kernel_indices.extend(['laplace', 'laplace', 'laplace']) - dir_vec_indices.extend([idx[1], idx[0], idx[2]]) - coeffs.extend([1 - 2*nu, -(1 - 2*nu), -(1 - 2*nu)]) - extra_deriv_dirs_vec.extend([[idx[0]], [idx[1]], [idx[2]]]) - if idx[0] != idx[1]: - coeffs[-1] = 0 - - if not self.use_biharmonic: - result = 0 - for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ - zip(kernel_indices, dir_vec_indices, coeffs, - extra_deriv_dirs_vec): - knl = self.kernel_dict[idx] - result += self._create_int_g(knl, deriv_dirs + extra_deriv_dirs, - density=density_sym*dir_vec_sym[dir_vec_idx], - use_source_deriv=False, - qbx_forced_limit=qbx_forced_limit) * coeff - return result/(2*(1 - nu)) - - result = 0 - for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ - zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): - result += self._get_int_g_deriv_relation(kernel_idx, - density_sym*dir_vec_sym[dir_vec_idx], - deriv_dirs + extra_deriv_dirs, coeff, - qbx_forced_limit=qbx_forced_limit) - - return result/(2*(1 - nu)) - - -class StokesletWrapper(StokesletWrapperBase, StokesletWrapperMixin): - def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): +class StokesletWrapper(StokesletWrapperBase): + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") - self.use_biharmonic = use_biharmonic - self.kernel_dict = {} - self.base_kernel = BiharmonicKernel(dim=dim) - for i in range(dim): for j in range(i, dim): self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, @@ -363,13 +275,16 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): for j in range(i): self.kernel_dict[(i, j)] = self.kernel_dict[(j, i)] - if self.use_biharmonic: - from pytential.symbolic.pde.system_utils import get_deriv_relation - results = get_deriv_relation(list(self.kernel_dict.values()), - self.base_kernel, tol=1e-10, order=2) - self.deriv_relation_dict = {} - for deriv_eq, idx in zip(results, self.kernel_dict.keys()): - self.deriv_relation_dict[idx] = deriv_eq + def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, + deriv_dirs): + """ + Returns the Integral of the Stokeslet/Stresslet kernel given by `idx` + and its derivatives. + """ + knl = self.kernel_dict[idx] + return _create_int_g(self.kernel_dict[idx], deriv_dirs, + density=density_sym*dir_vec_sym[idx[-1]], + qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) def apply(self, density_vec_sym, qbx_forced_limit): @@ -403,7 +318,6 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = StressletWrapper(dim=self.dim, - use_biharmonic=self.use_biharmonic, mu_sym=self.mu, nu_sym=self.nu) # For stokeslet, there's no direction vector involved @@ -423,7 +337,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): # {{{ StressletWrapper -class StressletWrapper(StressletWrapperBase, StokesletWrapperMixin): +class StressletWrapper(StressletWrapperBase): """Wrapper class for the :class:`~sumpy.kernel.StressletKernel` kernel. This class is meant to shield the user from the messiness of writing @@ -451,16 +365,13 @@ class StressletWrapper(StressletWrapperBase, StokesletWrapperMixin): .. automethod:: apply_stress """ - def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") - self.use_biharmonic = use_biharmonic self.kernel_dict = {} - self.base_kernel = BiharmonicKernel(dim=dim) - for i in range(dim): for j in range(i, dim): for k in range(j, dim): @@ -481,13 +392,37 @@ def __init__(self, dim=None, use_biharmonic=True, mu_sym=var("mu"), nu_sym=0.5): # For elasticity (nu != 0.5), we need the LaplaceKernel self.kernel_dict['laplace'] = LaplaceKernel(self.dim) - if self.use_biharmonic: - from pytential.symbolic.pde.system_utils import get_deriv_relation - results = get_deriv_relation(list(self.kernel_dict.values()), - self.base_kernel, tol=1e-10, order=3, verbose=False) - self.deriv_relation_dict = {} - for deriv_eq, (idx, knl) in zip(results, self.kernel_dict.items()): - self.deriv_relation_dict[idx] = deriv_eq + def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, + deriv_dirs): + """ + Returns the Integral of the Stresslet kernel given by `idx` + and its derivatives. + """ + + is_stresslet = (len(idx) == 3) + nu = self.nu + kernel_indices = [idx] + dir_vec_indices = [idx[-1]] + coeffs = [1] + extra_deriv_dirs_vec = [[]] + + kernel_indices = [idx, 'laplace', 'laplace', 'laplace'] + dir_vec_indices = [idx[-1], idx[1], idx[0], idx[2]] + coeffs = [1, 1 - 2*nu, -(1 - 2*nu), -(1 - 2*nu)] + extra_deriv_dirs_vec=[[], [idx[0]], [idx[1]], [idx[2]]] + if idx[0] != idx[1]: + coeffs[-1] = 0 + + result = 0 + for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ + zip(kernel_indices, dir_vec_indices, coeffs, + extra_deriv_dirs_vec): + knl = self.kernel_dict[kernel_idx] + result += _create_int_g(knl, deriv_dirs + extra_deriv_dirs, + density=density_sym*dir_vec_sym[dir_vec_idx], + qbx_forced_limit=qbx_forced_limit) * coeff + return result/(2*(1 - nu)) + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -587,12 +522,6 @@ def apply(self, density_vec_sym, qbx_forced_limit): return sym_expr - def get_laplace_int_g(self, density, qbx_forced_limit, deriv_dirs): - knl = self.kernel - for deriv_dir in deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - return sym.S(knl, density, qbx_forced_limit=qbx_forced_limit) - class StressletWrapperUsingLaplace(StokesletWrapperBase): """Stresslet Wrapper using Tornberg and Greengard's method which uses @@ -696,17 +625,19 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): self.stokeslet = StokesletWrapperUsingLaplace(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) elif method == "biharmonic" or method == "naive": - use_biharmonic = (method == "biharmonic") self.stresslet = StressletWrapper(dim=self.ambient_dim, - use_biharmonic=use_biharmonic, mu_sym=mu_sym, nu_sym=nu_sym) self.stokeslet = StokesletWrapper(dim=self.ambient_dim, - use_biharmonic=use_biharmonic, mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") + if method == "biharmonic": + self.base_kernel = BiharmonicKernel(dim=ambient_dim) + else: + self.base_kernel = None + @property def dim(self): return self.ambient_dim - 1 @@ -821,19 +752,20 @@ def _operator(self, sigma, normal, qbx_forced_limit): return op_k + self.eta * op_s def prepare_rhs(self, b): - return b + self._farfield(qbx_forced_limit=+1) + return merge_int_g_exprs(b + self._farfield(qbx_forced_limit=+1), + base_kernel=self.base_kernel) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. K. 1985 Equation 2.18 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator( - sigma, normal, qbx_forced_limit)) + sigma, normal, qbx_forced_limit), base_kernel=self.base_kernel) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 return merge_int_g_exprs( -self._farfield(qbx_forced_limit) - - self._operator(sigma, normal, qbx_forced_limit) - ) + - self._operator(sigma, normal, qbx_forced_limit), + base_kernel=self.base_kernel) def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: H. K. 1985 Equation 2.17 @@ -868,6 +800,7 @@ def __init__(self, *, eta=None, method="naive", mu_sym=var("mu"), nu_sym=0.5): eta = 0.75 self.eta = eta + self.laplace_kernel = LaplaceKernel(3) def _operator(self, sigma, normal, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit @@ -883,16 +816,16 @@ def _operator(self, sigma, normal, qbx_forced_limit): def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, - normal, qbx_forced_limit)) + normal, qbx_forced_limit), base_kernel=self.base_kernel, verbose=True) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 return merge_int_g_exprs(-self._operator(sigma, normal, - qbx_forced_limit)) + qbx_forced_limit), base_kernel=self.base_kernel) def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the # equivalent single-/double-layer pressure kernels raise NotImplementedError - + # }}} From 326d5b390669604d00e54ae53e8ed967f13d3c2c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 22 May 2021 12:16:27 -0500 Subject: [PATCH 045/209] Fix free space elasticity --- pytential/symbolic/stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 94105dde3..836784c2c 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -408,7 +408,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, kernel_indices = [idx, 'laplace', 'laplace', 'laplace'] dir_vec_indices = [idx[-1], idx[1], idx[0], idx[2]] - coeffs = [1, 1 - 2*nu, -(1 - 2*nu), -(1 - 2*nu)] + coeffs = [1, (1 - 2*nu)/self.dim, -(1 - 2*nu)/self.dim, -(1 - 2*nu)] extra_deriv_dirs_vec=[[], [idx[0]], [idx[1]], [idx[2]]] if idx[0] != idx[1]: coeffs[-1] = 0 From 2b77a34acf873df4c592469389d8d76f70c15190 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 22 May 2021 14:42:29 -0500 Subject: [PATCH 046/209] degree -> order --- pytential/symbolic/pde/system_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 086a4e7d2..26eeca21c 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -66,11 +66,11 @@ def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): pde = base_kernel.get_pde_as_diff_op() if order is None: - order = pde.degree + order = pde.order - if order > pde.degree: + if order > pde.order: raise NotImplementedError(f"order ({order}) cannot be greater than the order" - f"of the PDE ({pde.degree}) yet.") + f"of the PDE ({pde.order}) yet.") mis = sorted(gnitstam(order, dim), key=sum) # (-1, -1, -1) represent a constant From e11a8944b661daebc11e6c6390e3f937693aff9f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 26 May 2021 15:42:56 -0500 Subject: [PATCH 047/209] Implement Yoshida et al algorithm --- pytential/symbolic/elasticity.py | 352 ++++++++++++++++++------- pytential/symbolic/pde/system_utils.py | 2 +- 2 files changed, 262 insertions(+), 92 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 2cc5fe375..d8be6708e 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -25,13 +25,53 @@ from pytential import sym from pytential.symbolic.pde.system_utils import merge_int_g_exprs from sumpy.kernel import (LineOfCompressionKernel, - AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) + AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier, + LaplaceKernel, BiharmonicKernel) from pymbolic import var -from pytential.symbolic.stokes import HebekerExteriorStokesOperator, StokesOperator +from pytential.symbolic.stokes import ( + StokesletWrapperTornberg, StressletWrapper, StokesletWrapper, + StokesletWrapperBase) from pytential.symbolic.primitives import NodeCoordinateComponent -class MindlinOperator(StokesOperator): +class KelvinOperator: + + def __init__(self, method, mu_sym, nu_sym): + if nu_sym == 0.5: + raise ValueError("poisson's ratio cannot be 0.5") + + self.dim = 3 + self.mu = mu_sym + self.nu = nu_sym + + if method == "laplace": + self.stresslet = StressletWrapperYoshida(dim=self.dim, + mu_sym=mu_sym, nu_sym=nu_sym) + self.stokeslet = StokesletWrapperTornberg(dim=self.dim, + mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "biharmonic": + self.stresslet = StressletWrapper(dim=self.dim, + mu_sym=mu_sym, nu_sym=nu_sym) + self.stokeslet = StokesletWrapper(dim=self.dim, + mu_sym=mu_sym, nu_sym=nu_sym) + else: + raise ValueError(f"invalid method: {method}." + "Needs to be one of naive, laplace, biharmonic") + + if method == "biharmonic": + self.base_kernel = BiharmonicKernel(dim=self.dim) + else: + self.base_kernel = None + + self.laplace_kernel = LaplaceKernel(dim=self.dim) + + def operator(self, sigma, *, normal, qbx_forced_limit="avg"): + return merge_int_g_exprs(self.stresslet.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit), + base_kernel=self.base_kernel) + + +class MindlinOperator: """Representation for 3D Stokes Flow based on [Hebeker1986]_. Inherits from :class:`StokesOperator`. @@ -52,58 +92,79 @@ def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu")): self.method = method self.mu = mu_sym self.nu = nu_sym - self._stokes = HebekerExteriorStokesOperator(eta=0, method=method, - mu_sym=mu_sym, nu_sym=nu_sym) - self._stokes_a = HebekerExteriorStokesOperator(eta=0, method=method, - mu_sym=mu_sym + 4*nu_sym, nu_sym=-nu_sym) + self.free_space_op = KelvinOperator(method=method, mu_sym=mu_sym, + nu_sym=nu_sym) + self.modified_free_space_op = KelvinOperator( + method=method, mu_sym=mu_sym + 4*nu_sym, nu_sym=-nu_sym) self.dim = 3 self.compression_knl = LineOfCompressionKernel(self.dim, 2, mu_sym, nu_sym) def K(self, sigma, normal, qbx_forced_limit): - return merge_int_g_exprs(self._stokes.stresslet.apply(sigma, normal, + return merge_int_g_exprs(self.free_space_op.stresslet.apply(sigma, normal, qbx_forced_limit=qbx_forced_limit)) def A(self, sigma, normal, qbx_forced_limit): - result = -self._stokes_a.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit) + result = -self.modified_free_space_op.stresslet.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit) + new_density = sum(a*b for a, b in zip(sigma, normal)) + int_g = sym.S(self.free_space_op.laplace_kernel, new_density, + qbx_forced_limit=qbx_forced_limit) + for i in range(self.dim): - result[i] += 2*self._create_int_g(self._stokes.laplace_kernel, [], [i], - new_density, qbx_forced_limit) + temp = 2*int_g.copy( + target_kernel=AxisTargetDerivative(i, int_g.target_kernel)) + if i == 2: + temp *= -1 + result[i] += temp return result - def B(self, sigma, normal, qbx_forced_limit): + def _create_int_g(self, knl, source_deriv_dirs, target_deriv_dirs, density, + **kwargs): - def create_int_g(density, source_dirs, target_dir): - knl = self.compression_knl - for source_dir in source_dirs: - knl = AxisSourceDerivative(source_dir, knl) - knl = AxisTargetDerivative(target_dir, knl) - kwargs = {} - args = [arg.loopy_arg.name for arg in knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) + for deriv_dir in target_deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + if deriv_dir == 2: + density *= -1 + + args = [arg.loopy_arg.name for arg in knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) - return sym.int_g_vec(knl, density, qbx_forced_limit=qbx_forced_limit, - **kwargs) + res = sym.S(knl, density, **kwargs) + return res + def B(self, sigma, normal, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) mu = self.mu nu = self.nu lam = 2*nu*mu/(1-2*nu) sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) + source_kernel_dirs = [[0, 0], [1, 1], [2, 2], [0, 1]] + densities = [ + sigma[0]*normal[0]*2*mu, + sigma[1]*normal[1]*2*mu, + -sigma[2]*normal[2]*2*mu - 2*lam*sigma_normal_product, + (sigma[0]*normal[1] + sigma[1]*normal[0])*2*mu, + ] + source_kernels = [ + AxisSourceDerivative(a, AxisSourceDerivative(b, self.compression_knl)) + for a, b in source_kernel_dirs + ] + + kwargs = {"qbx_forced_limit": qbx_forced_limit} + args = [arg.loopy_arg.name for arg in self.compression_knl.get_args()] + for arg in args: + kwargs[arg] = var(arg) + + int_g = sym.IntG(source_kernels=tuple(source_kernels), + target_kernel=self.compression_knl, densities=tuple(densities), + **kwargs) + for i in range(self.dim): - val = self._create_int_g(sigma[0]*normal[0], [0, 0], [i]) - val += self._create_int_g(sigma[1]*normal[1], [1, 1], [i]) - val -= self._create_int_g(sigma[2]*normal[2], [2, 2], [i]) - val += self._create_int_g(sigma[0]*normal[1]+sigma[1]*normal[0], - [0, 1], [i]) - val *= 2*mu - val -= 2*lam*create_int_g(sigma_normal_product, [2, 2], i) - if i == 2: - val *= -1 - sym_expr[i] = val + sym_expr[i] = int_g.copy(target_kernel=AxisTargetDerivative( + i, int_g.target_kernel)) return sym_expr @@ -116,74 +177,183 @@ def C(self, sigma, normal, qbx_forced_limit): y = [NodeCoordinateComponent(i) for i in range(self.dim)] sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) - def get_laplace_int_g(density, source_deriv_dirs, target_deriv_dirs=[]): - int_g = self._create_int_g(self._stokes.laplace_kernel, - source_deriv_dirs, target_deriv_dirs, density, + laplace_kernel = self.free_space_op.laplace_kernel + densities = [] + source_kernels = [] + + # phi_c in Gimbutas et, al. + densities.extend([ + -2*alpha*mu*y[2]*sigma[0]*normal[0], + -2*alpha*mu*y[2]*sigma[1]*normal[1], + -2*alpha*mu*y[2]*sigma[2]*normal[2], + -2*alpha*mu*y[2]*(sigma[0]*normal[1] + sigma[1]*normal[0]), + +2*alpha*mu*y[2]*(sigma[0]*normal[2] + sigma[2]*normal[0]), + +2*alpha*mu*y[2]*(sigma[1]*normal[2] + sigma[2]*normal[1]), + ]) + source_kernel_dirs = [[0, 0], [1, 1], [2, 2], [0, 1], [0, 2], [1, 2]] + source_kernels.extend([ + AxisSourceDerivative(a, AxisSourceDerivative(b, laplace_kernel)) + for a, b in source_kernel_dirs + ]) + + # G in Gimbutas et, al. + densities.extend([ + (2*alpha - 2)*y[2]*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), + (2*alpha - 2)*y[2]*mu*(sigma[1]*normal[2] + sigma[2]*normal[1]), + (2*alpha - 2)*y[2]*(mu*-2*sigma[2]*normal[2] + - lam*sigma_normal_product), + ]) + source_kernels.extend( + [AxisSourceDerivative(i, laplace_kernel) for i in range(3)]) + + int_g = sym.IntG(source_kernels=tuple(source_kernels), + target_kernel=laplace_kernel, densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) - return int_g.copy( - target_kernel=TargetPointMultiplier(2, int_g.target_kernel)) for i in range(self.dim): - # phi_c in Gimbutas et, al. - temp = get_laplace_int_g(y[2]*sigma[0]*normal[0], [0, 0], [i]) - temp += get_laplace_int_g(y[2]*sigma[1]*normal[1], [1, 1], [i]) - temp += get_laplace_int_g(y[2]*sigma[2]*normal[2], [2, 2], [i]) - temp += get_laplace_int_g(y[2]*(sigma[0]*normal[1] + sigma[1]*normal[0]), - [0, 1], [i]) - temp -= get_laplace_int_g(y[2]*(sigma[0]*normal[2] + sigma[2]*normal[0]), - [0, 2], [i]) - temp -= get_laplace_int_g(y[2]*(sigma[1]*normal[2] + sigma[2]*normal[1]), - [1, 2], [i]) - temp *= -2*alpha*mu - result[i] += temp + result[i] = int_g.copy(target_kernel=AxisTargetDerivative( + i, int_g.target_kernel)) - # G in Gimbutas et, al. - temp = get_laplace_int_g( - y[2]*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), [0], [i]) - temp += get_laplace_int_g( - y[2]*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), [1], [i]) - temp += get_laplace_int_g( - y[2]*(mu*-2*sigma[2]*normal[2] - lam*sigma_normal_product), [2], [i]) - temp *= (2*alpha - 2) - result[i] += temp + if i == 2: + # Target derivative w.r.t x[2] is flipped due to target image + result[i] *= -1 # H in Gimubtas et, al. - temp = get_laplace_int_g(sigma[0]*normal[0], [0, 0]) - temp += get_laplace_int_g(sigma[1]*normal[1], [1, 1]) - temp += get_laplace_int_g(sigma[2]*normal[2], [2, 2]) - temp += get_laplace_int_g(sigma[0]*normal[1] + sigma[1]*normal[0], [0, 1]) - temp -= get_laplace_int_g(sigma[0]*normal[2] + sigma[2]*normal[0], [0, 2]) - temp -= get_laplace_int_g(sigma[1]*normal[2] + sigma[2]*normal[1], [1, 2]) - - result[2] -= -2*(2 - alpha)*mu*temp + densities = [ + (-2)*(2 - alpha)*mu*sigma[0]*normal[0], + (-2)*(2 - alpha)*mu*sigma[1]*normal[1], + (-2)*(2 - alpha)*mu*sigma[2]*normal[2], + (-2)*(2 - alpha)*mu*(sigma[0]*normal[1] + sigma[1]*normal[0]), + (+2)*(2 - alpha)*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), + (+2)*(2 - alpha)*mu*(sigma[1]*normal[2] + sigma[2]*normal[1]), + ] + source_kernel_dirs = [[0, 0], [1, 1], [2, 2], [0, 1], [0, 2], [1, 2]] + source_kernels = [ + AxisSourceDerivative(a, AxisSourceDerivative(b, laplace_kernel)) + for a, b in source_kernel_dirs + ] + H = sym.IntG(source_kernels=tuple(source_kernels), + target_kernel=laplace_kernel, densities=tuple(densities), + qbx_forced_limit=qbx_forced_limit) + result[2] -= H return result + def free_space_operator(self, sigma, *, normal, qbx_forced_limit="avg"): + return self.free_space_op.operator(sigma=sigma, normal=normal, + qbx_forced_limit=qbx_forced_limit) + def operator(self, sigma, *, normal, qbx_forced_limit="avg"): - # A and C are both derivatives of Biharmonic Green's function - # TODO: make merge_int_g_exprs smart enough to merge two different - # kernels into two separate IntGs. - result = self.A(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - result += self.C(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - result = merge_int_g_exprs(result, base_kernel=self._stokes.base_kernel) - - result += merge_int_g_exprs(self.B(sigma, normal=normal, - qbx_forced_limit=qbx_forced_limit), base_kernel=self.compression_knl) - return result + resultA = self.A(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + resultB = self.C(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + resultC = self.B(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + + if self.method == "biharmonic": + # A and C are both derivatives of Biharmonic Green's function + # TODO: make merge_int_g_exprs smart enough to merge two different + # kernels into two separate IntGs. + result = merge_int_g_exprs(resultA + resultB, + base_kernel=self.free_space_op.base_kernel) + result += merge_int_g_exprs(resultC, base_kernel=self.compression_knl) + return result + else: + return resultA + resultB + resultC + + def get_density_var(self, name="sigma"): + """ + :returns: a symbolic vector corresponding to the density. + """ + return sym.make_sym_vector(name, self.dim) + + +class StressletWrapperYoshida(StokesletWrapperBase): + """Stresslet Wrapper using Yoshida et al's method [1] which uses Laplace + derivatives. + + [1] Yoshida, K. I., Nishimura, N., & Kobayashi, S. (2001). Application of + fast multipole Galerkin boundary integral equation method to elastostatic + crack problems in 3D. + International Journal for Numerical Methods in Engineering, 50(3), 525-547. + """ - def _create_int_g(self, knl, source_deriv_dirs, target_deriv_dirs, density, - **kwargs): - for deriv_dir in source_deriv_dirs: - knl = AxisSourceDerivative(deriv_dir, knl) + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + self.dim = dim + if dim != 3: + raise ValueError("unsupported dimension given to " + "StressletWrapperYoshida") + self.kernel = LaplaceKernel(dim=self.dim) + self.mu = mu_sym + self.nu = nu_sym - for deriv_dir in target_deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - if deriv_dir == 2: - density *= -1 + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): - args = [arg.loopy_arg.name for arg in knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) + mu = self.mu + nu = self.nu + lam = 2*nu*mu/(1-2*nu) - res = sym.S(knl, density, **kwargs) - return res + def C(i, j, k, l): # noqa: E741 + res = 0 + if i == j and k == l: + res += lam + if i == k and j == l: + res += mu + if i == l and j == k: + res += mu + return res + + def P(i, j, int_g): + res = -int_g.copy(target_kernel=TargetPointMultiplier(j, + AxisTargetDerivative(i, int_g.target_kernel))) + if i == j: + res += (3 - 4*nu)*int_g + return res / (4*mu*(1 - nu)) + + def Q(i, int_g): + res = int_g.copy(target_kernel=AxisTargetDerivative(i, + int_g.target_kernel)) + return res / (4*mu*(1 - nu)) + + sym_expr = np.zeros((self.dim,), dtype=object) + + kernel = self.kernel + source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + normal = dir_vec_sym + sigma = density_vec_sym + + for i in range(3): + for k in range(3): + source_kernels = [None]*3 + densities = [0]*3 + for l in range(3): # noqa: E741 + source_kernels[l] = AxisSourceDerivative(l, kernel) + for j in range(3): + for m in range(3): + densities[l] += C(k, l, m, j)*normal[m]*sigma[j] + int_g = sym.IntG(target_kernel=kernel, + source_kernels=tuple(source_kernels), + densities=tuple(densities), + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += P(i, j, int_g) + + source_kernels = [None]*4 + densities = [0]*4 + for l in range(3): # noqa: E741 + source_kernels[l] = AxisSourceDerivative(l, kernel) + source_kernels[3] = kernel + + for k in range(3): + for m in range(3): + for j in range(3): + for l in range(3): # noqa: E741 + densities[l] += \ + C(k, l, m, j)*normal[m]*sigma[j]*source[k] + if k == l: + densities[3] += \ + C(k, l, m, j)*normal[m]*sigma[j] + int_g = sym.IntG(target_kernel=kernel, + source_kernels=tuple(source_kernels), + densities=tuple(densities), + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += Q(i, int_g) + + return sym_expr diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 26eeca21c..f5782830f 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -76,7 +76,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): # (-1, -1, -1) represent a constant mis.append((-1, -1, -1)) - if order == pde.degree: + if order == pde.order: pde_mis = [ident.mi for eq in pde.eqs for ident in eq.keys()] pde_mis = [mi for mi in pde_mis if sum(mi) == order] if verbose: From dd937fb5a12356b103d9a4b6d32ff8b932d7a1fd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 26 May 2021 17:31:07 -0500 Subject: [PATCH 048/209] Add 9 harmonic implementation --- pytential/symbolic/stokes.py | 74 ++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 836784c2c..1e2822aa6 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -24,7 +24,7 @@ from pytential import sym from pytential.symbolic.pde.system_utils import merge_int_g_exprs -from sumpy.kernel import (StokesletKernel, StressletKernel, LaplaceKernel, +from sumpy.kernel import (StressletKernel, LaplaceKernel, ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from pymbolic import var @@ -281,7 +281,6 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, Returns the Integral of the Stokeslet/Stresslet kernel given by `idx` and its derivatives. """ - knl = self.kernel_dict[idx] return _create_int_g(self.kernel_dict[idx], deriv_dirs, density=density_sym*dir_vec_sym[idx[-1]], qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) @@ -399,7 +398,6 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, and its derivatives. """ - is_stresslet = (len(idx) == 3) nu = self.nu kernel_indices = [idx] dir_vec_indices = [idx[-1]] @@ -409,7 +407,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, kernel_indices = [idx, 'laplace', 'laplace', 'laplace'] dir_vec_indices = [idx[-1], idx[1], idx[0], idx[2]] coeffs = [1, (1 - 2*nu)/self.dim, -(1 - 2*nu)/self.dim, -(1 - 2*nu)] - extra_deriv_dirs_vec=[[], [idx[0]], [idx[1]], [idx[2]]] + extra_deriv_dirs_vec = [[], [idx[0]], [idx[1]], [idx[2]]] if idx[0] != idx[1]: coeffs[-1] = 0 @@ -423,7 +421,6 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, qbx_forced_limit=qbx_forced_limit) * coeff return result/(2*(1 - nu)) - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) @@ -484,7 +481,7 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # {{{ Stokeslet/Stresslet using Laplace -class StokesletWrapperUsingLaplace(StokesletWrapperBase): +class StokesletWrapperTornberg(StokesletWrapperBase): """Stokeslet Wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -514,16 +511,16 @@ def apply(self, density_vec_sym, qbx_forced_limit): sym_expr[i] -= sym.S(knl, density_vec_sym[j], qbx_forced_limit=qbx_forced_limit) if i == j: - sym_expr[i] += sym.S(self.kernel, density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += (3 - 4 * self.nu)*sym.S(self.kernel, + density_vec_sym[j], qbx_forced_limit=qbx_forced_limit) sym_expr[i] += sym.S(AxisTargetDerivative(i, self.kernel), common_expr_density, qbx_forced_limit=qbx_forced_limit) - sym_expr[i] *= -0.5*(self.mu*(-1)) + sym_expr[i] *= -0.25*((self.mu*(1 - self.nu_sym))**(-1)) return sym_expr -class StressletWrapperUsingLaplace(StokesletWrapperBase): +class StressletWrapperTornberg(StokesletWrapperBase): """Stresslet Wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -544,24 +541,24 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + common_source_kernels = tuple([AxisSourceDerivative(k, self.kernel) for + k in range(self.dim)]) + coeff = 3.0/6 for i in range(self.dim): for j in range(self.dim): - source_kernels = [AxisSourceDerivative(k, self.kernel) for - k in range(self.dim)] - densities = [density_vec_sym[k] * dir_vec_sym[j] - + density_vec_sym[j] * dir_vec_sym[k] - for k in range(self.dim)] + densities = [coeff*(density_vec_sym[k] * dir_vec_sym[j] + + density_vec_sym[j] * dir_vec_sym[k]) for k in range(self.dim)] target_kernel = TargetPointMultiplier(j, AxisTargetDerivative(i, self.kernel)) sym_expr[i] -= sym.IntG(target_kernel=target_kernel, - source_kernels=source_kernels, - densities=densities, + source_kernels=common_source_kernels, + densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) if i == j: sym_expr[i] += sym.IntG(target_kernel=self.kernel, - source_kernels=source_kernels, + source_kernels=common_source_kernels, densities=densities, qbx_forced_limit=qbx_forced_limit) @@ -569,19 +566,38 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): k in range(self.dim)) common_density1 = sum(source[k] * dir_vec_sym[k] for k in range(self.dim)) - source_kernels = [AxisSourceDerivative(k, self.kernel) for - k in range(self.dim)] - densities = [common_density0 * dir_vec_sym[k] - + common_density1 * density_vec_sym[k] for + densities = [coeff*(common_density0 * dir_vec_sym[k] + + common_density1 * density_vec_sym[k]) for k in range(self.dim)] + source_kernels = list(common_source_kernels) + + if self.nu != 0.5: + sigma_normal = [density_vec_sym[k]*dir_vec_sym[k] for + k in range(self.dim)] + densities.append((1 - 2 * self.nu)*( + sum(sigma_normal) - 2 * sigma_normal[i])) + source_kernels.append(self.kernel) - target_kernel = AxisTargetDerivative(i, self.kernel) sym_expr[i] += sym.IntG(target_kernel=target_kernel, - source_kernels=source_kernels, - densities=densities, + source_kernels=tuple(source_kernels), + densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) - sym_expr[i] *= 3.0/6 + for j in range(self.dim): + if self.nu != 0.5 and i != j: + i0, j0 = min(i, j), max(i, j) + densities = ((1 - 2*self.nu)*(-1)*( + dir_vec_sym[i0]*density_vec_sym[j0] + + dir_vec_sym[j0]*density_vec_sym[i0]),) + source_kernels = (self.kernel,) + target_kernel = AxisTargetDerivative(j, self.kernel) + + sym_expr[i] += sym.IntG(target_kernel=target_kernel, + source_kernels=tuple(source_kernels), + densities=tuple(densities), + qbx_forced_limit=qbx_forced_limit) + + sym_expr *= 1/(2*(1 - self.nu)) return sym_expr @@ -620,9 +636,9 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): self.nu = nu_sym if method == "laplace": - self.stresslet = StressletWrapperUsingLaplace(dim=self.ambient_dim, + self.stresslet = StressletWrapperTornberg(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapperUsingLaplace(dim=self.ambient_dim, + self.stokeslet = StokesletWrapperTornberg(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) elif method == "biharmonic" or method == "naive": self.stresslet = StressletWrapper(dim=self.ambient_dim, @@ -827,5 +843,5 @@ def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the # equivalent single-/double-layer pressure kernels raise NotImplementedError - + # }}} From 850a71f28272bf9d8c670ce209f955f08b81833e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 26 May 2021 17:32:54 -0500 Subject: [PATCH 049/209] Remove 9 FMM implementationa in favour of 4 FMMs --- pytential/symbolic/stokes.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 1e2822aa6..07f956207 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -532,6 +532,8 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to StressletWrapper") + if nu_sym != 0.5: + raise ValueError("nu != 0.5 is not supported") self.kernel = LaplaceKernel(dim=self.dim) self.mu = mu_sym self.nu = nu_sym @@ -571,34 +573,11 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): k in range(self.dim)] source_kernels = list(common_source_kernels) - if self.nu != 0.5: - sigma_normal = [density_vec_sym[k]*dir_vec_sym[k] for - k in range(self.dim)] - densities.append((1 - 2 * self.nu)*( - sum(sigma_normal) - 2 * sigma_normal[i])) - source_kernels.append(self.kernel) - sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=tuple(source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) - for j in range(self.dim): - if self.nu != 0.5 and i != j: - i0, j0 = min(i, j), max(i, j) - densities = ((1 - 2*self.nu)*(-1)*( - dir_vec_sym[i0]*density_vec_sym[j0] - + dir_vec_sym[j0]*density_vec_sym[i0]),) - source_kernels = (self.kernel,) - target_kernel = AxisTargetDerivative(j, self.kernel) - - sym_expr[i] += sym.IntG(target_kernel=target_kernel, - source_kernels=tuple(source_kernels), - densities=tuple(densities), - qbx_forced_limit=qbx_forced_limit) - - sym_expr *= 1/(2*(1 - self.nu)) - return sym_expr From 1ebcd512fcf80df84ad4c9efd4cd5d864046a146 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 26 May 2021 17:43:32 -0500 Subject: [PATCH 050/209] Add references --- pytential/symbolic/elasticity.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index d8be6708e..165c18d2e 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -35,6 +35,16 @@ class KelvinOperator: + """Representation for free space Green's function for elasticity commonly + known as the Kelvin solution [1] given by Lord Kelvin. + + [1] Gimbutas, Z., & Greengard, L. (2016). A fast multipole method for the + evaluation of elastostatic fields in a half-space with zero normal stress. + Advances in Computational Mathematics, 42(1), 175-198. + + .. automethod:: __init__ + .. automethod:: operator + """ def __init__(self, method, mu_sym, nu_sym): if nu_sym == 0.5: @@ -72,16 +82,20 @@ def operator(self, sigma, *, normal, qbx_forced_limit="avg"): class MindlinOperator: - """Representation for 3D Stokes Flow based on [Hebeker1986]_. + """Representation for elasticity in a half-space with zero normal stress which + is based on Mindlin's explicit solution. See [1] and [2]. - Inherits from :class:`StokesOperator`. + [1] Mindlin, R. D. (1936). Force at a point in the interior of a semi‐infinite + solid. Physics, 7(5), 195-202. - .. [Hebeker1986] F. C. Hebeker, *Efficient Boundary Element Methods for - Three-Dimensional Exterior Viscous Flow*, Numerical Methods for - Partial Differential Equations, Vol. 2, 1986, - `DOI `__. + [2] Gimbutas, Z., & Greengard, L. (2016). A fast multipole method for the + evaluation of elastostatic fields in a half-space with zero normal stress. + Advances in Computational Mathematics, 42(1), 175-198. .. automethod:: __init__ + .. automethod:: operator + .. automethod:: free_space_operator + .. automethod:: get_density_var """ def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu")): From 9b14abc4ee758bbb9f873df99d8c839ee6529f0a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 27 May 2021 14:21:52 -0500 Subject: [PATCH 051/209] Fix names --- pytential/symbolic/elasticity.py | 70 ++++++++++++-------------------- 1 file changed, 27 insertions(+), 43 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 165c18d2e..9a1c81565 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -26,15 +26,14 @@ from pytential.symbolic.pde.system_utils import merge_int_g_exprs from sumpy.kernel import (LineOfCompressionKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier, - LaplaceKernel, BiharmonicKernel) + LaplaceKernel) from pymbolic import var -from pytential.symbolic.stokes import ( - StokesletWrapperTornberg, StressletWrapper, StokesletWrapper, - StokesletWrapperBase) +from pytential.symbolic.stokes import (StokesletWrapperTornberg, + StokesletWrapperBase, HebekerExteriorStokesOperator) from pytential.symbolic.primitives import NodeCoordinateComponent -class KelvinOperator: +class KelvinOperator(HebekerExteriorStokesOperator): """Representation for free space Green's function for elasticity commonly known as the Kelvin solution [1] given by Lord Kelvin. @@ -46,34 +45,20 @@ class KelvinOperator: .. automethod:: operator """ - def __init__(self, method, mu_sym, nu_sym): + def __init__(self, method="native", mu_sym=var("mu"), nu_sym=0.5): if nu_sym == 0.5: raise ValueError("poisson's ratio cannot be 0.5") - self.dim = 3 - self.mu = mu_sym - self.nu = nu_sym - + super().__init__(eta=0, method="biharmonic", mu_sym=mu_sym, nu_sym=nu_sym) if method == "laplace": - self.stresslet = StressletWrapperYoshida(dim=self.dim, + self.stresslet = StressletWrapperYoshida(dim=3, mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapperTornberg(dim=self.dim, + self.stokeslet = StokesletWrapperTornberg(dim=3, mu_sym=mu_sym, nu_sym=nu_sym) - elif method == "biharmonic": - self.stresslet = StressletWrapper(dim=self.dim, - mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapper(dim=self.dim, - mu_sym=mu_sym, nu_sym=nu_sym) - else: - raise ValueError(f"invalid method: {method}." - "Needs to be one of naive, laplace, biharmonic") - - if method == "biharmonic": - self.base_kernel = BiharmonicKernel(dim=self.dim) - else: self.base_kernel = None - - self.laplace_kernel = LaplaceKernel(dim=self.dim) + elif method != "biharmonic": + raise ValueError(f"invalid method: {method}." + "Needs to be one of laplace or biharmonic") def operator(self, sigma, *, normal, qbx_forced_limit="avg"): return merge_int_g_exprs(self.stresslet.apply(sigma, normal, @@ -110,8 +95,8 @@ def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu")): nu_sym=nu_sym) self.modified_free_space_op = KelvinOperator( method=method, mu_sym=mu_sym + 4*nu_sym, nu_sym=-nu_sym) - self.dim = 3 - self.compression_knl = LineOfCompressionKernel(self.dim, 2, mu_sym, nu_sym) + self.compression_knl = LineOfCompressionKernel(3, 2, mu_sym, nu_sym) + self.ambient_dim = 3 def K(self, sigma, normal, qbx_forced_limit): return merge_int_g_exprs(self.free_space_op.stresslet.apply(sigma, normal, @@ -125,7 +110,7 @@ def A(self, sigma, normal, qbx_forced_limit): int_g = sym.S(self.free_space_op.laplace_kernel, new_density, qbx_forced_limit=qbx_forced_limit) - for i in range(self.dim): + for i in range(3): temp = 2*int_g.copy( target_kernel=AxisTargetDerivative(i, int_g.target_kernel)) if i == 2: @@ -149,7 +134,7 @@ def _create_int_g(self, knl, source_deriv_dirs, target_deriv_dirs, density, return res def B(self, sigma, normal, qbx_forced_limit): - sym_expr = np.zeros((self.dim,), dtype=object) + sym_expr = np.zeros((3,), dtype=object) mu = self.mu nu = self.nu lam = 2*nu*mu/(1-2*nu) @@ -176,19 +161,19 @@ def B(self, sigma, normal, qbx_forced_limit): target_kernel=self.compression_knl, densities=tuple(densities), **kwargs) - for i in range(self.dim): + for i in range(3): sym_expr[i] = int_g.copy(target_kernel=AxisTargetDerivative( i, int_g.target_kernel)) return sym_expr def C(self, sigma, normal, qbx_forced_limit): - result = np.zeros((self.dim,), dtype=object) + result = np.zeros((3,), dtype=object) mu = self.mu nu = self.nu lam = 2*nu*mu/(1-2*nu) alpha = (lam + mu)/(lam + 2*mu) - y = [NodeCoordinateComponent(i) for i in range(self.dim)] + y = [NodeCoordinateComponent(i) for i in range(3)] sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) laplace_kernel = self.free_space_op.laplace_kernel @@ -224,7 +209,7 @@ def C(self, sigma, normal, qbx_forced_limit): target_kernel=laplace_kernel, densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) - for i in range(self.dim): + for i in range(3): result[i] = int_g.copy(target_kernel=AxisTargetDerivative( i, int_g.target_kernel)) @@ -259,16 +244,16 @@ def free_space_operator(self, sigma, *, normal, qbx_forced_limit="avg"): def operator(self, sigma, *, normal, qbx_forced_limit="avg"): resultA = self.A(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - resultB = self.C(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - resultC = self.B(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + resultC = self.C(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) + resultB = self.B(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) if self.method == "biharmonic": # A and C are both derivatives of Biharmonic Green's function # TODO: make merge_int_g_exprs smart enough to merge two different # kernels into two separate IntGs. - result = merge_int_g_exprs(resultA + resultB, + result = merge_int_g_exprs(resultA + resultC, base_kernel=self.free_space_op.base_kernel) - result += merge_int_g_exprs(resultC, base_kernel=self.compression_knl) + result += merge_int_g_exprs(resultB, base_kernel=self.compression_knl) return result else: return resultA + resultB + resultC @@ -277,7 +262,7 @@ def get_density_var(self, name="sigma"): """ :returns: a symbolic vector corresponding to the density. """ - return sym.make_sym_vector(name, self.dim) + return sym.make_sym_vector(name, 3) class StressletWrapperYoshida(StokesletWrapperBase): @@ -291,11 +276,10 @@ class StressletWrapperYoshida(StokesletWrapperBase): """ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): - self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " "StressletWrapperYoshida") - self.kernel = LaplaceKernel(dim=self.dim) + self.kernel = LaplaceKernel(dim=3) self.mu = mu_sym self.nu = nu_sym @@ -327,10 +311,10 @@ def Q(i, int_g): int_g.target_kernel)) return res / (4*mu*(1 - nu)) - sym_expr = np.zeros((self.dim,), dtype=object) + sym_expr = np.zeros((3,), dtype=object) kernel = self.kernel - source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + source = [sym.NodeCoordinateComponent(d) for d in range(3)] normal = dir_vec_sym sigma = density_vec_sym From 4285e84c2cda9b63e0168b10d33f4b51d7e624c6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 27 May 2021 16:29:21 -0500 Subject: [PATCH 052/209] Revert "Remove 9 FMM implementationa in favour of 4 FMMs" This reverts commit 850a71f28272bf9d8c670ce209f955f08b81833e. --- pytential/symbolic/stokes.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 07f956207..1e2822aa6 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -532,8 +532,6 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to StressletWrapper") - if nu_sym != 0.5: - raise ValueError("nu != 0.5 is not supported") self.kernel = LaplaceKernel(dim=self.dim) self.mu = mu_sym self.nu = nu_sym @@ -573,11 +571,34 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): k in range(self.dim)] source_kernels = list(common_source_kernels) + if self.nu != 0.5: + sigma_normal = [density_vec_sym[k]*dir_vec_sym[k] for + k in range(self.dim)] + densities.append((1 - 2 * self.nu)*( + sum(sigma_normal) - 2 * sigma_normal[i])) + source_kernels.append(self.kernel) + sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=tuple(source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) + for j in range(self.dim): + if self.nu != 0.5 and i != j: + i0, j0 = min(i, j), max(i, j) + densities = ((1 - 2*self.nu)*(-1)*( + dir_vec_sym[i0]*density_vec_sym[j0] + + dir_vec_sym[j0]*density_vec_sym[i0]),) + source_kernels = (self.kernel,) + target_kernel = AxisTargetDerivative(j, self.kernel) + + sym_expr[i] += sym.IntG(target_kernel=target_kernel, + source_kernels=tuple(source_kernels), + densities=tuple(densities), + qbx_forced_limit=qbx_forced_limit) + + sym_expr *= 1/(2*(1 - self.nu)) + return sym_expr From 07e0159286df8d2489c84590ff949a9ee8ea203f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 27 May 2021 17:06:44 -0500 Subject: [PATCH 053/209] Revert "Revert "Remove 9 FMM implementationa in favour of 4 FMMs"" This reverts commit 4285e84c2cda9b63e0168b10d33f4b51d7e624c6. --- pytential/symbolic/stokes.py | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 1e2822aa6..07f956207 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -532,6 +532,8 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to StressletWrapper") + if nu_sym != 0.5: + raise ValueError("nu != 0.5 is not supported") self.kernel = LaplaceKernel(dim=self.dim) self.mu = mu_sym self.nu = nu_sym @@ -571,34 +573,11 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): k in range(self.dim)] source_kernels = list(common_source_kernels) - if self.nu != 0.5: - sigma_normal = [density_vec_sym[k]*dir_vec_sym[k] for - k in range(self.dim)] - densities.append((1 - 2 * self.nu)*( - sum(sigma_normal) - 2 * sigma_normal[i])) - source_kernels.append(self.kernel) - sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=tuple(source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) - for j in range(self.dim): - if self.nu != 0.5 and i != j: - i0, j0 = min(i, j), max(i, j) - densities = ((1 - 2*self.nu)*(-1)*( - dir_vec_sym[i0]*density_vec_sym[j0] - + dir_vec_sym[j0]*density_vec_sym[i0]),) - source_kernels = (self.kernel,) - target_kernel = AxisTargetDerivative(j, self.kernel) - - sym_expr[i] += sym.IntG(target_kernel=target_kernel, - source_kernels=tuple(source_kernels), - densities=tuple(densities), - qbx_forced_limit=qbx_forced_limit) - - sym_expr *= 1/(2*(1 - self.nu)) - return sym_expr From f193f09dfc13082d4bc265d44a618016a1d9b548 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 28 May 2021 02:12:12 -0500 Subject: [PATCH 054/209] Finish free space elasticity and add testing --- pytential/symbolic/elasticity.py | 28 ++++++++++--------- pytential/symbolic/stokes.py | 10 +++++-- test/test_stokes.py | 46 ++++++++++++++++++++++++-------- 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 9a1c81565..faff201bb 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -29,11 +29,11 @@ LaplaceKernel) from pymbolic import var from pytential.symbolic.stokes import (StokesletWrapperTornberg, - StokesletWrapperBase, HebekerExteriorStokesOperator) + StokesletWrapperBase, StokesOperator) from pytential.symbolic.primitives import NodeCoordinateComponent -class KelvinOperator(HebekerExteriorStokesOperator): +class KelvinOperator(StokesOperator): """Representation for free space Green's function for elasticity commonly known as the Kelvin solution [1] given by Lord Kelvin. @@ -45,26 +45,28 @@ class KelvinOperator(HebekerExteriorStokesOperator): .. automethod:: operator """ - def __init__(self, method="native", mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, method="laplace", mu_sym=var("mu"), nu_sym=0.5): if nu_sym == 0.5: raise ValueError("poisson's ratio cannot be 0.5") - super().__init__(eta=0, method="biharmonic", mu_sym=mu_sym, nu_sym=nu_sym) - if method == "laplace": - self.stresslet = StressletWrapperYoshida(dim=3, - mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapperTornberg(dim=3, - mu_sym=mu_sym, nu_sym=nu_sym) - self.base_kernel = None - elif method != "biharmonic": - raise ValueError(f"invalid method: {method}." + if method not in ["laplace", "biharmonic"]: + raise ValueError(f"invalid method: {method}. " "Needs to be one of laplace or biharmonic") + super().__init__(ambient_dim=3, side=+1, method=method, + mu_sym=mu_sym, nu_sym=nu_sym) + self.laplace_kernel = LaplaceKernel(3) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): return merge_int_g_exprs(self.stresslet.apply(sigma, normal, qbx_forced_limit=qbx_forced_limit), base_kernel=self.base_kernel) + def get_density_var(self, name="sigma"): + """ + :returns: a symbolic vector corresponding to the density. + """ + return sym.make_sym_vector(name, 3) + class MindlinOperator: """Representation for elasticity in a half-space with zero normal stress which @@ -331,7 +333,7 @@ def Q(i, int_g): source_kernels=tuple(source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) - sym_expr[i] += P(i, j, int_g) + sym_expr[i] += P(i, k, int_g) source_kernels = [None]*4 densities = [0]*4 diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 07f956207..673718a92 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -515,7 +515,7 @@ def apply(self, density_vec_sym, qbx_forced_limit): density_vec_sym[j], qbx_forced_limit=qbx_forced_limit) sym_expr[i] += sym.S(AxisTargetDerivative(i, self.kernel), common_expr_density, qbx_forced_limit=qbx_forced_limit) - sym_expr[i] *= -0.25*((self.mu*(1 - self.nu_sym))**(-1)) + sym_expr[i] *= -0.25*((self.mu*(1 - self.nu))**(-1)) return sym_expr @@ -573,6 +573,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): k in range(self.dim)] source_kernels = list(common_source_kernels) + target_kernel = AxisTargetDerivative(i, self.kernel) sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=tuple(source_kernels), densities=tuple(densities), @@ -605,6 +606,7 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. """ + from pytential.symbolic.elasticity import StressletWrapperYoshida if side not in [+1, -1]: raise ValueError(f"invalid evaluation side: {side}") @@ -615,7 +617,11 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): self.nu = nu_sym if method == "laplace": - self.stresslet = StressletWrapperTornberg(dim=self.ambient_dim, + if nu_sym == 0.5: + self.stresslet = StressletWrapperTornberg(dim=self.ambient_dim, + mu_sym=mu_sym, nu_sym=nu_sym) + else: + self.stresslet = StressletWrapperYoshida(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) self.stokeslet = StokesletWrapperTornberg(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) diff --git a/test/test_stokes.py b/test/test_stokes.py index 3cb0fa812..6d1d5b332 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -50,6 +50,7 @@ def run_exterior_stokes(ctx_factory, *, source_ovsmp=None, radius=1.5, mu=1.0, + nu=0.4, visualize=False, method="naive", @@ -135,13 +136,20 @@ def run_exterior_stokes(ctx_factory, *, sym_normal = sym.make_sym_vector("normal", ambient_dim) sym_mu = sym.var("mu") + if nu == 0.5: + sym_nu = 0.5 + else: + sym_nu = sym.var("nu") + if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator - sym_omega = sym.make_sym_vector("omega", ambient_dim, mu_sym=sym_mu) - op = HsiaoKressExteriorStokesOperator(omega=sym_omega, method=method) + sym_omega = sym.make_sym_vector("omega", ambient_dim) + op = HsiaoKressExteriorStokesOperator(omega=sym_omega, method=method, + mu_sym=sym_mu, nu_sym=sym_nu) elif ambient_dim == 3: from pytential.symbolic.stokes import HebekerExteriorStokesOperator - op = HebekerExteriorStokesOperator(method=method, mu_sym=sym_mu) + op = HebekerExteriorStokesOperator(method=method, + mu_sym=sym_mu, nu_sym=sym_nu) else: assert False @@ -155,7 +163,7 @@ def run_exterior_stokes(ctx_factory, *, from pytential.symbolic.stokes import StokesletWrapper sym_source_pot = StokesletWrapper(ambient_dim, - use_biharmonic=False).apply(sym_sigma, qbx_forced_limit=None) + nu_sym=sym_nu).apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -181,10 +189,16 @@ def run_exterior_stokes(ctx_factory, *, else: bc_context = {} op_context = {"mu": mu, "normal": normal} + direct_context = {"mu": mu} + + if sym_nu != 0.5: + bc_context["nu"] = nu + op_context["nu"] = nu + direct_context["nu"] = nu bc_op = bind(places, sym_source_pot, auto_where=("point_source", "source")) - bc = bc_op(actx, sigma=charges, mu=mu) + bc = bc_op(actx, sigma=charges, **direct_context) rhs = bind(places, sym_rhs)(actx, bc=bc, **bc_context) bound_op = bind(places, sym_op) @@ -197,9 +211,12 @@ def run_exterior_stokes(ctx_factory, *, def print_timing_data(timings, name): result = {k: 0 for k in list(timings.values())[0].keys()} + total = 0 for k, timing in timings.items(): for k, v in timing.items(): result[k] += v['wall_elapsed'] + total += v['wall_elapsed'] + result['total'] = total print(f"{name}={result}") print_timing_data(fmm_timing_data, method) @@ -232,7 +249,8 @@ def rnorm2(x, y): ps_velocity = bind(places, sym_velocity, auto_where=("source", "point_target"))(actx, sigma=sigma, **op_context) ex_velocity = bind(places, sym_source_pot, - auto_where=("point_source", "point_target"))(actx, sigma=charges, mu=mu) + auto_where=("point_source", "point_target"))(actx, sigma=charges, + **direct_context) v_error = rnorm2(ps_velocity, ex_velocity) h_max = bind(places, sym.h_max(ambient_dim))(actx) @@ -263,13 +281,18 @@ def rnorm2(x, y): return h_max, v_error +@pytest.mark.parametrize("ambient_dim, method, nu", [ + (2, "naive", 0.5), + (2, "biharmonic", 0.5), + pytest.param(3, "naive", 0.5, marks=pytest.mark.slowtest), + (3, "biharmonic", 0.5), + (3, "laplace", 0.5), -@pytest.mark.parametrize("method", ["naive", "biharmonic"]) -@pytest.mark.parametrize("ambient_dim", [ - 2, - pytest.param(3, marks=pytest.mark.slowtest) + (2, "biharmonic", 0.4), + (3, "biharmonic", 0.4), + (3, "laplace", 0.4), ]) -def test_exterior_stokes(ctx_factory, ambient_dim, method="naive", visualize=False): +def test_exterior_stokes(ctx_factory, ambient_dim, method, nu, visualize=False): if visualize: logging.basicConfig(level=logging.INFO) @@ -296,6 +319,7 @@ def test_exterior_stokes(ctx_factory, ambient_dim, method="naive", visualize=Fal qbx_order=qbx_order, resolution=resolution, visualize=visualize, + nu=nu, method=method) eoc.add_data_point(h_max, err) From 11ae160e4e1c422c51f03a55cde64b4e787a631a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 9 Jun 2021 18:04:38 -0500 Subject: [PATCH 055/209] Make free space elasticity faster with Laplace --- pytential/symbolic/elasticity.py | 38 +++++---- pytential/symbolic/primitives.py | 7 +- pytential/symbolic/stokes.py | 127 +++++++++++++------------------ 3 files changed, 79 insertions(+), 93 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index faff201bb..12344f832 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -28,8 +28,7 @@ AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier, LaplaceKernel) from pymbolic import var -from pytential.symbolic.stokes import (StokesletWrapperTornberg, - StokesletWrapperBase, StokesOperator) +from pytential.symbolic.stokes import StokesletWrapperBase, StokesOperator from pytential.symbolic.primitives import NodeCoordinateComponent @@ -85,7 +84,8 @@ class MindlinOperator: .. automethod:: get_density_var """ - def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu")): + def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu"), + line_of_compression_tol=0): if method not in ["biharmonic", "laplace"]: raise ValueError(f"invalid method: {method}." "Needs to be one of laplace, biharmonic") @@ -97,7 +97,8 @@ def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu")): nu_sym=nu_sym) self.modified_free_space_op = KelvinOperator( method=method, mu_sym=mu_sym + 4*nu_sym, nu_sym=-nu_sym) - self.compression_knl = LineOfCompressionKernel(3, 2, mu_sym, nu_sym) + self.compression_knl = LineOfCompressionKernel(3, 2, mu_sym, nu_sym, + tol=line_of_compression_tol) self.ambient_dim = 3 def K(self, sigma, normal, qbx_forced_limit): @@ -286,6 +287,12 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.nu = nu_sym def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + return self.apply_stokeslet_and_stresslet([0]*self.dim, + density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1) + + def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, + stresslet_density_vec_sym, dir_vec_sym, + qbx_forced_limit, stokeslet_weight, stresslet_weight): mu = self.mu nu = self.nu @@ -299,7 +306,7 @@ def C(i, j, k, l): # noqa: E741 res += mu if i == l and j == k: res += mu - return res + return res * stresslet_weight def P(i, j, int_g): res = -int_g.copy(target_kernel=TargetPointMultiplier(j, @@ -318,29 +325,28 @@ def Q(i, int_g): kernel = self.kernel source = [sym.NodeCoordinateComponent(d) for d in range(3)] normal = dir_vec_sym - sigma = density_vec_sym + sigma = stresslet_density_vec_sym + + source_kernels = [None]*4 + densities = [0]*4 + for i in range(3): + source_kernels[i] = AxisSourceDerivative(i, kernel) + source_kernels[3] = kernel for i in range(3): for k in range(3): - source_kernels = [None]*3 - densities = [0]*3 + densities = [0]*4 for l in range(3): # noqa: E741 - source_kernels[l] = AxisSourceDerivative(l, kernel) for j in range(3): for m in range(3): densities[l] += C(k, l, m, j)*normal[m]*sigma[j] + densities[3] += stokeslet_weight * stokeslet_density_vec_sym[k] int_g = sym.IntG(target_kernel=kernel, source_kernels=tuple(source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) sym_expr[i] += P(i, k, int_g) - source_kernels = [None]*4 - densities = [0]*4 - for l in range(3): # noqa: E741 - source_kernels[l] = AxisSourceDerivative(l, kernel) - source_kernels[3] = kernel - for k in range(3): for m in range(3): for j in range(3): @@ -350,6 +356,8 @@ def Q(i, int_g): if k == l: densities[3] += \ C(k, l, m, j)*normal[m]*sigma[j] + densities[3] += stokeslet_weight * source[k] \ + * stokeslet_density_vec_sym[k] int_g = sym.IntG(target_kernel=kernel, source_kernels=tuple(source_kernels), densities=tuple(densities), diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 2ad5a948a..2ad0c7d53 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1500,8 +1500,11 @@ def __init__(self, target_kernel, source_kernels, densities, raise ValueError("invalid value (%s) of qbx_forced_limit" % qbx_forced_limit) - source_kernels = tuple(source_kernels) - densities = tuple(densities) + densities = list(densities) + source_kernels = tuple([source_kernels[i] for i in range(len(densities)) \ + if densities[i] != 0]) + densities = tuple([density for density in densities if density != 0]) + kernel_arg_names = set() for kernel in source_kernels + (target_kernel,): diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 673718a92..bb8fc9288 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -434,6 +434,21 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): return sym_expr + def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, + stresslet_density_vec_sym, dir_vec_sym, + qbx_forced_limit, stokeslet_weight, stresslet_weight): + + stokeslet_obj = StokesletWrapper(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu) + + sym_expr = self.apply(stresslet_density_vec_sym, dir_vec_sym, + qbx_forced_limit) * stresslet_weight + sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, + qbx_forced_limit) * stokeslet_weight + + return sym_expr + + def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -481,48 +496,9 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # {{{ Stokeslet/Stresslet using Laplace -class StokesletWrapperTornberg(StokesletWrapperBase): - """Stokeslet Wrapper using Tornberg and Greengard's method which uses - Laplace derivatives. - - [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the - three-dimensional Stokes equations. - Journal of Computational Physics, 227(3), 1613-1619. - """ - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): - self.dim = dim - if dim != 3: - raise ValueError("unsupported dimension given to StokesletWrapper") - self.kernel = LaplaceKernel(dim=self.dim) - self.mu = mu_sym - self.nu = nu_sym - - def apply(self, density_vec_sym, qbx_forced_limit): - - sym_expr = np.zeros((self.dim,), dtype=object) - - source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] - common_expr_density = sum(source[k]*density_vec_sym[k] for - k in range(self.dim)) - - for i in range(self.dim): - for j in range(self.dim): - knl = TargetPointMultiplier(j, AxisTargetDerivative(i, self.kernel)) - sym_expr[i] -= sym.S(knl, density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit) - if i == j: - sym_expr[i] += (3 - 4 * self.nu)*sym.S(self.kernel, - density_vec_sym[j], qbx_forced_limit=qbx_forced_limit) - sym_expr[i] += sym.S(AxisTargetDerivative(i, self.kernel), - common_expr_density, qbx_forced_limit=qbx_forced_limit) - sym_expr[i] *= -0.25*((self.mu*(1 - self.nu))**(-1)) - - return sym_expr - - class StressletWrapperTornberg(StokesletWrapperBase): - """Stresslet Wrapper using Tornberg and Greengard's method which uses - Laplace derivatives. + """A Stresslet wrapper using Tornberg and Greengard's method which + uses Laplace derivatives. [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the three-dimensional Stokes equations. @@ -539,22 +515,34 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.nu = nu_sym def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + return self.apply_stokeslet_and_stresslet([0]*self.dim, + density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1) + + def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, + stresslet_density_vec_sym, dir_vec_sym, + qbx_forced_limit, stokeslet_weight, stresslet_weight): sym_expr = np.zeros((self.dim,), dtype=object) source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] - common_source_kernels = tuple([AxisSourceDerivative(k, self.kernel) for - k in range(self.dim)]) - coeff = 3.0/6 + common_source_kernels = [AxisSourceDerivative(k, self.kernel) for + k in range(self.dim)] + common_source_kernels.append(self.kernel) + + stokeslet_weight *= 3.0/6 + stresslet_weight *= -0.5*self.mu**(-1) for i in range(self.dim): for j in range(self.dim): - densities = [coeff*(density_vec_sym[k] * dir_vec_sym[j] - + density_vec_sym[j] * dir_vec_sym[k]) for k in range(self.dim)] + densities = [stresslet_weight*( + stresslet_density_vec_sym[k] * dir_vec_sym[j] + + stresslet_density_vec_sym[j] * dir_vec_sym[k]) \ + for k in range(self.dim)] + densities.append(stokeslet_weight*stokeslet_density_vec_sym[j]) target_kernel = TargetPointMultiplier(j, AxisTargetDerivative(i, self.kernel)) sym_expr[i] -= sym.IntG(target_kernel=target_kernel, - source_kernels=common_source_kernels, + source_kernels=tuple(common_source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) @@ -564,18 +552,20 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): densities=densities, qbx_forced_limit=qbx_forced_limit) - common_density0 = sum(source[k] * density_vec_sym[k] for + common_density0 = sum(source[k] * stresslet_density_vec_sym[k] for k in range(self.dim)) common_density1 = sum(source[k] * dir_vec_sym[k] for k in range(self.dim)) - densities = [coeff*(common_density0 * dir_vec_sym[k] - + common_density1 * density_vec_sym[k]) for + common_density2 = sum(source[k] * stokeslet_density_vec_sym[k] for + k in range(self.dim)) + densities = [stresslet_weight*(common_density0 * dir_vec_sym[k] + + common_density1 * stresslet_density_vec_sym[k]) for k in range(self.dim)] - source_kernels = list(common_source_kernels) + densities.append(common_density2) target_kernel = AxisTargetDerivative(i, self.kernel) sym_expr[i] += sym.IntG(target_kernel=target_kernel, - source_kernels=tuple(source_kernels), + source_kernels=tuple(common_source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) @@ -623,13 +613,9 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): else: self.stresslet = StressletWrapperYoshida(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapperTornberg(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) elif method == "biharmonic" or method == "naive": self.stresslet = StressletWrapper(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapper(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -724,9 +710,9 @@ def __init__(self, *, omega, alpha=None, eta=None, method="naive", def _farfield(self, qbx_forced_limit): source_dofdesc = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) length = sym.integral(self.ambient_dim, self.dim, 1, dofdesc=source_dofdesc) - return self.stokeslet.apply( - -self.omega / length, - qbx_forced_limit=qbx_forced_limit) + return self.stresslet.apply( + -self.omega / length, [0]*self.dim, [0]*self.dim, + qbx_forced_limit, 1, 0) def _operator(self, sigma, normal, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit @@ -742,15 +728,11 @@ def _operator(self, sigma, normal, qbx_forced_limit): meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, self.dim, sigma, dofdesc=dd)) - op_k = self.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit) - op_s = ( - self.alpha / (2.0 * np.pi) * int_sigma - - self.stokeslet.apply(meanless_sigma, - qbx_forced_limit=slp_qbx_forced_limit) - ) + result = self.eta * self.alpha / (2.0 * np.pi) * int_sigma + result += self.stresslet.apply_stokeslet_and_stresslet(meanless_sigma, + sigma, normal, qbx_forced_limit, -self.eta, 1) - return op_k + self.eta * op_s + return result def prepare_rhs(self, b): return merge_int_g_exprs(b + self._farfield(qbx_forced_limit=+1), @@ -804,15 +786,8 @@ def __init__(self, *, eta=None, method="naive", mu_sym=var("mu"), nu_sym=0.5): self.laplace_kernel = LaplaceKernel(3) def _operator(self, sigma, normal, qbx_forced_limit): - slp_qbx_forced_limit = qbx_forced_limit - # if slp_qbx_forced_limit == "avg": - # slp_qbx_forced_limit = self.side - - op_w = self.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit) - op_v = self.stokeslet.apply(sigma, qbx_forced_limit=slp_qbx_forced_limit) - - return op_w + self.eta * op_v + return self.stresslet.apply_stokeslet_and_stresslet(sigma, + sigma, normal, qbx_forced_limit, self.eta, 1) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 From 880b3b6967b6e83cd3fe601eb84c1ba1c32257bf Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 09:33:48 -0500 Subject: [PATCH 056/209] Make method=laplace the default --- pytential/symbolic/stokes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index bb8fc9288..4eeb3ef5a 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -679,7 +679,7 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=None, eta=None, method="naive", + def __init__(self, *, omega, alpha=None, eta=None, method="laplace", mu_sym=var("mu"), nu_sym=0.5): r""" :arg omega: farfield behaviour of the velocity field, as defined @@ -768,7 +768,7 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None, method="naive", mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, *, eta=None, method="laplace", mu_sym=var("mu"), nu_sym=0.5): r""" :arg eta: a parameter :math:`\eta > 0`. Choosing this parameter well can have a non-trivial effect on the conditioning of the operator. From 2c0359e81f2c513b01f675a07ed868cd2e40f148 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 10:10:59 -0500 Subject: [PATCH 057/209] Make apply_derivative and apply_pressure fast --- pytential/symbolic/elasticity.py | 22 ++++++--- pytential/symbolic/stokes.py | 83 +++++++++----------------------- 2 files changed, 39 insertions(+), 66 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 12344f832..8bb64a2c3 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -286,13 +286,16 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=[]): return self.apply_stokeslet_and_stresslet([0]*self.dim, - density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1) + density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1, + extra_deriv_dirs) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, - qbx_forced_limit, stokeslet_weight, stresslet_weight): + qbx_forced_limit, stokeslet_weight, stresslet_weight, + extra_deriv_dirs): mu = self.mu nu = self.nu @@ -308,16 +311,23 @@ def C(i, j, k, l): # noqa: E741 res += mu return res * stresslet_weight + def add_extra_deriv_dirs(target_kernel): + for deriv_dir in extra_deriv_dirs: + target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) + return target_kernel + def P(i, j, int_g): + int_g = int_g.copy(target_kernel=add_extra_deriv_dirs( + int_g.target_kernel)) res = -int_g.copy(target_kernel=TargetPointMultiplier(j, - AxisTargetDerivative(i, int_g.target_kernel))) + AxisTargetDerivative(i, int_g.target_kernel))) if i == j: res += (3 - 4*nu)*int_g return res / (4*mu*(1 - nu)) def Q(i, int_g): - res = int_g.copy(target_kernel=AxisTargetDerivative(i, - int_g.target_kernel)) + res = int_g.copy(target_kernel=add_extra_deriv_dirs( + AxisTargetDerivative(i, int_g.target_kernel)) return res / (4*mu*(1 - nu)) sym_expr = np.zeros((3,), dtype=object) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 4eeb3ef5a..5dd9a4a03 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -73,7 +73,7 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, qbx_forced_limit): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stokeslet kernel. Returns an object array of symbolic expressions for the vector @@ -97,7 +97,7 @@ def apply_pressure(self, density_vec_sym, qbx_forced_limit): sym.S(kernel, density_vec_sym[i], qbx_forced_limit=qbx_forced_limit))) - return sym_expr + return merge_int_g_exprs(sym_expr) def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): """Symbolic derivative of velocity from Stokeslet. @@ -111,7 +111,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ - raise NotImplementedError + return self.apply(density_vec_sym, qbx_forced_limit, [deriv_dir]) def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. @@ -170,7 +170,7 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stresslet kernel. Returns an object array of symbolic expressions for the vector @@ -201,24 +201,7 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): density_vec_sym[i] * dir_vec_sym[j], qbx_forced_limit=qbx_forced_limit))) - return sym_expr - - def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, - qbx_forced_limit): - """Symbolic derivative of velocity from stresslet. - - Returns an object array of symbolic expressions for the vector - resulting from integrating the *deriv_dir* target derivative of the - dyadic Stresslet kernel with variable *density_vec_sym* and source - direction vectors *dir_vec_sym*. - - :arg deriv_dir: integer denoting the axis direction for the derivative. - :arg density_vec_sym: a symbolic vector variable for the density vector. - :arg dir_vec_sym: a symbolic vector variable for the normal direction. - :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on - to :class:`~pytential.symbolic.primitives.IntG`. - """ - raise NotImplementedError + return merge_int_g_exprs(sym_expr) def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -285,7 +268,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, density=density_sym*dir_vec_sym[idx[-1]], qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) - def apply(self, density_vec_sym, qbx_forced_limit): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): sym_expr = np.zeros((self.dim,), dtype=object) @@ -295,21 +278,7 @@ def apply(self, density_vec_sym, qbx_forced_limit): for i in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i), density_vec_sym[i], [1]*self.dim, - qbx_forced_limit, deriv_dirs=[]) - - return sym_expr - - def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): - - sym_expr = self.apply(density_vec_sym, qbx_forced_limit) - - # For stokeslet, there's no direction vector involved - # passing a list of ones instead to remove its usage. - for comp in range(self.dim): - for i in range(self.dim): - sym_expr[comp] += self.get_int_g((comp, i), - density_vec_sym[i], [1]*self.dim, - qbx_forced_limit, deriv_dirs=[deriv_dir]) + qbx_forced_limit, deriv_dirs=extra_deriv_dirs) return sym_expr @@ -421,7 +390,8 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, qbx_forced_limit=qbx_forced_limit) * coeff return result/(2*(1 - nu)) - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=[]): sym_expr = np.zeros((self.dim,), dtype=object) @@ -430,39 +400,26 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): for j in range(self.dim): sym_expr[comp] += self.get_int_g((comp, i, j), density_vec_sym[i], dir_vec_sym, - qbx_forced_limit, deriv_dirs=[]) + qbx_forced_limit, deriv_dirs=extra_deriv_dirs) return sym_expr def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, - qbx_forced_limit, stokeslet_weight, stresslet_weight): + qbx_forced_limit, stokeslet_weight, stresslet_weight, + extra_deriv_dirs=[]): stokeslet_obj = StokesletWrapper(dim=self.dim, mu_sym=self.mu, nu_sym=self.nu) sym_expr = self.apply(stresslet_density_vec_sym, dir_vec_sym, - qbx_forced_limit) * stresslet_weight + qbx_forced_limit, extra_deriv_dirs) * stresslet_weight sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, - qbx_forced_limit) * stokeslet_weight + qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight return sym_expr - def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, - qbx_forced_limit): - - sym_expr = np.zeros((self.dim,), dtype=object) - - for comp in range(self.dim): - for i in range(self.dim): - for j in range(self.dim): - sym_expr[comp] += self.get_int_g((comp, i, j), - density_vec_sym[i], dir_vec_sym, - qbx_forced_limit, deriv_dirs=[deriv_dir]) - - return sym_expr - def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -514,13 +471,15 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=[]): return self.apply_stokeslet_and_stresslet([0]*self.dim, - density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1) + density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1, extra_deriv_dirs) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, - qbx_forced_limit, stokeslet_weight, stresslet_weight): + qbx_forced_limit, stokeslet_weight, stresslet_weight, + extra_deriv_dirs): sym_expr = np.zeros((self.dim,), dtype=object) @@ -541,6 +500,8 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, densities.append(stokeslet_weight*stokeslet_density_vec_sym[j]) target_kernel = TargetPointMultiplier(j, AxisTargetDerivative(i, self.kernel)) + for deriv_dir in extra_deriv_dirs: + target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) sym_expr[i] -= sym.IntG(target_kernel=target_kernel, source_kernels=tuple(common_source_kernels), densities=tuple(densities), @@ -564,6 +525,8 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, densities.append(common_density2) target_kernel = AxisTargetDerivative(i, self.kernel) + for deriv_dir in extra_deriv_dirs: + target_kernel = AxisTargetDerivative(deriv_dir, targer_kernel) sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=tuple(common_source_kernels), densities=tuple(densities), From dc1459498080b7c2f8d050e4de39e66b7958c7ed Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 10:20:11 -0500 Subject: [PATCH 058/209] Add StokesletWrapperTornberg --- pytential/symbolic/elasticity.py | 34 ++++++++++++++++-- pytential/symbolic/stokes.py | 62 ++++++++++++++++---------------- 2 files changed, 64 insertions(+), 32 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 8bb64a2c3..829a79772 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -28,7 +28,8 @@ AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier, LaplaceKernel) from pymbolic import var -from pytential.symbolic.stokes import StokesletWrapperBase, StokesOperator +from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase, + StokesOperator) from pytential.symbolic.primitives import NodeCoordinateComponent @@ -268,7 +269,7 @@ def get_density_var(self, name="sigma"): return sym.make_sym_vector(name, 3) -class StressletWrapperYoshida(StokesletWrapperBase): +class StressletWrapperYoshida(StressletWrapperBase): """Stresslet Wrapper using Yoshida et al's method [1] which uses Laplace derivatives. @@ -279,6 +280,7 @@ class StressletWrapperYoshida(StokesletWrapperBase): """ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " "StressletWrapperYoshida") @@ -375,3 +377,31 @@ def Q(i, int_g): sym_expr[i] += Q(i, int_g) return sym_expr + + +class StokesletWrapperYoshida(StokesletWrapperBase): + """Stokeslet Wrapper using Yoshida et al's method [1] which uses Laplace + derivatives. + + [1] Yoshida, K. I., Nishimura, N., & Kobayashi, S. (2001). Application of + fast multipole Galerkin boundary integral equation method to elastostatic + crack problems in 3D. + International Journal for Numerical Methods in Engineering, 50(3), 525-547. + """ + + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + self.dim = dim + if dim != 3: + raise ValueError("unsupported dimension given to " + "StokesletWrapperYoshida") + self.kernel = LaplaceKernel(dim=3) + self.mu = mu_sym + self.nu = nu_sym + + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=[]): + stresslet = StressletWrapperYoshida(dim=3, self.mu, self.nu) + return self.apply_stokeslet_and_stresslet(density_vec_sym, + [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, + extra_deriv_dirs) + diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 5dd9a4a03..458e4b9d3 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -306,33 +306,6 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): # {{{ StressletWrapper class StressletWrapper(StressletWrapperBase): - """Wrapper class for the :class:`~sumpy.kernel.StressletKernel` kernel. - - This class is meant to shield the user from the messiness of writing - out every term in the expansion of the triple-indexed Stresslet - kernel applied to both a normal vector and the density vector. - The object is created to do some of the set-up and bookkeeping once, - rather than every time we want to create a symbolic expression based - on the kernel -- say, once when we solve for the density, and once when - we want a symbolic representation for the solution, for example. - - The :meth:`apply` function returns the integral expressions needed for - convolving the kernel with a vector density, and is meant to work - similarly to :func:`~pytential.symbolic.primitives.S` (which is - :class:`~pytential.symbolic.primitives.IntG`). - - Similar functions are available for other useful things related to - the flow: :meth:`apply_pressure`, :meth:`apply_derivative` (target derivative), - :meth:`apply_stress` (applies symmetric viscous stress tensor in - the requested direction). - - .. automethod:: __init__ - .. automethod:: apply - .. automethod:: apply_pressure - .. automethod:: apply_derivative - .. automethod:: apply_stress - """ - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): @@ -453,7 +426,7 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # {{{ Stokeslet/Stresslet using Laplace -class StressletWrapperTornberg(StokesletWrapperBase): +class StressletWrapperTornberg(StressletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -464,7 +437,8 @@ class StressletWrapperTornberg(StokesletWrapperBase): def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.dim = dim if dim != 3: - raise ValueError("unsupported dimension given to StressletWrapper") + raise ValueError("unsupported dimension given to " + "StressletWrapperTornberg") if nu_sym != 0.5: raise ValueError("nu != 0.5 is not supported") self.kernel = LaplaceKernel(dim=self.dim) @@ -535,6 +509,34 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, return sym_expr +class StokesletWrapperTornberg(StokesletWrapperBase): + """A Stresslet wrapper using Tornberg and Greengard's method which + uses Laplace derivatives. + + [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the + three-dimensional Stokes equations. + Journal of Computational Physics, 227(3), 1613-1619. + """ + + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + self.dim = dim + if dim != 3: + raise ValueError("unsupported dimension given to " + "StokesletWrapperTornberg") + if nu_sym != 0.5: + raise ValueError("nu != 0.5 is not supported") + self.kernel = LaplaceKernel(dim=self.dim) + self.mu = mu_sym + self.nu = nu_sym + + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=[]): + stresslet = StressletWrapperTornberg(dim=3, self.mu, self.nu) + return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, + [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, + extra_deriv_dirs) + + # }}} @@ -642,7 +644,7 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=None, eta=None, method="laplace", + def __init__(self, *, omega, alpha=None, eta=None, method="biharmonic", mu_sym=var("mu"), nu_sym=0.5): r""" :arg omega: farfield behaviour of the velocity field, as defined From 4852ff47f9c1274b25bb52ca64da57fd902cdda5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 11:50:39 -0500 Subject: [PATCH 059/209] Keep stokeslet object --- pytential/symbolic/stokes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 458e4b9d3..173d28b9f 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -561,7 +561,8 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. """ - from pytential.symbolic.elasticity import StressletWrapperYoshida + from pytential.symbolic.elasticity import (StressletWrapperYoshida, + StokesletWrapperYoshida) if side not in [+1, -1]: raise ValueError(f"invalid evaluation side: {side}") @@ -575,12 +576,18 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): if nu_sym == 0.5: self.stresslet = StressletWrapperTornberg(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) + self.stokeslet = StokesletWrapperTornberg(dim=self.ambient_dim, + mu_sym=mu_sym, nu_sym=nu_sym) else: self.stresslet = StressletWrapperYoshida(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) + self.stokeslet = StokesletWrapperYoshida(dim=self.ambient_dim, + mu_sym=mu_sym, nu_sym=nu_sym) elif method == "biharmonic" or method == "naive": self.stresslet = StressletWrapper(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym) + self.stokeslet = StokesletWrapper(dim=self.ambient_dim, + mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") From eccf2123a421f830513c21b1669d6ddc758d38b0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 13:30:09 -0500 Subject: [PATCH 060/209] update experiments/stokes-2d-interior.py to use biharmonic --- experiments/stokes-2d-interior.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/experiments/stokes-2d-interior.py b/experiments/stokes-2d-interior.py index 44828a6b1..5a9ce1577 100644 --- a/experiments/stokes-2d-interior.py +++ b/experiments/stokes-2d-interior.py @@ -21,6 +21,8 @@ qbx_order = 2 fmm_order = 7 mu = 3 +# method has to be one of biharmonic/naive for 2D +method = "biharmonic" # Test solution type -- either 'fundamental' or 'couette' (default is couette) soln_type = 'couette' @@ -87,7 +89,7 @@ def get_obj_array(obj_array): loc_sign = -1 # Create stresslet object - stresslet_obj = StressletWrapper(dim=2, mu_sym=mu_sym) + stresslet_obj = StressletWrapper(dim=2, mu_sym=mu_sym, method=method) # Describe boundary operator bdry_op_sym = loc_sign * 0.5 * sigma_sym + sqrt_w * stresslet_obj.apply(inv_sqrt_w_sigma, nvec_sym, qbx_forced_limit='avg') From fb319bae01b1094edd313422ee2fc59e2280087d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 14:37:03 -0500 Subject: [PATCH 061/209] use method in StokesletWrapper too --- pytential/symbolic/elasticity.py | 6 +-- pytential/symbolic/stokes.py | 70 ++++++++++++++++++++------------ test/test_stokes.py | 4 +- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 829a79772..e8ca67f80 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -297,7 +297,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, - extra_deriv_dirs): + extra_deriv_dirs=[]): mu = self.mu nu = self.nu @@ -329,7 +329,7 @@ def P(i, j, int_g): def Q(i, int_g): res = int_g.copy(target_kernel=add_extra_deriv_dirs( - AxisTargetDerivative(i, int_g.target_kernel)) + AxisTargetDerivative(i, int_g.target_kernel))) return res / (4*mu*(1 - nu)) sym_expr = np.zeros((3,), dtype=object) @@ -400,7 +400,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): - stresslet = StressletWrapperYoshida(dim=3, self.mu, self.nu) + stresslet = StressletWrapperYoshida(3, self.mu, self.nu) return self.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 173d28b9f..483e46eeb 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,6 +27,7 @@ from sumpy.kernel import (StressletKernel, LaplaceKernel, ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) +from sumpy.tools import get_all_variables from pymbolic import var __doc__ = """ @@ -73,6 +74,9 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym + args = list(get_all_variables(self.mu)) + list(get_all_variables(self.nu)) + self.extra_kwargs = {arg.name: arg for arg in args} + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stokeslet kernel. @@ -170,6 +174,9 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym + args = list(get_all_variables(self.nu)) + self.extra_kwargs = {arg.name: arg for arg in args} + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stresslet kernel. @@ -239,11 +246,19 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): class StokesletWrapper(StokesletWrapperBase): - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5, method="biharmonic"): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") + self.method = method + if method == "biharmonic": + self.base_kernel = BiharmonicKernel(dim) + elif method == "naive": + self.base_kernel = None + else: + raise ValueError("method has to be one of biharmonic/naive") + self.kernel_dict = {} for i in range(dim): @@ -266,7 +281,8 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, """ return _create_int_g(self.kernel_dict[idx], deriv_dirs, density=density_sym*dir_vec_sym[idx[-1]], - qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) + qbx_forced_limit=qbx_forced_limit, + **self.extra_kwargs)/(2*(1-self.nu)) def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): @@ -280,13 +296,13 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): density_vec_sym[i], [1]*self.dim, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - return sym_expr + return merge_int_g_exprs(sym_expr, base_kernel=self.base_kernel) def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = StressletWrapper(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu) + mu_sym=self.mu, nu_sym=self.nu, method=self.method) # For stokeslet, there's no direction vector involved # passing a list of ones instead to remove its usage. @@ -306,11 +322,19 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): # {{{ StressletWrapper class StressletWrapper(StressletWrapperBase): - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5, method="biharmonic"): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") + self.method = method + if method == "biharmonic": + self.base_kernel = BiharmonicKernel(dim) + elif method == "naive": + self.base_kernel = None + else: + raise ValueError("method has to be one of biharmonic/naive") + self.kernel_dict = {} for i in range(dim): @@ -360,7 +384,8 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, knl = self.kernel_dict[kernel_idx] result += _create_int_g(knl, deriv_dirs + extra_deriv_dirs, density=density_sym*dir_vec_sym[dir_vec_idx], - qbx_forced_limit=qbx_forced_limit) * coeff + qbx_forced_limit=qbx_forced_limit, + **self.extra_kwargs) * coeff return result/(2*(1 - nu)) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, @@ -375,7 +400,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, density_vec_sym[i], dir_vec_sym, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - return sym_expr + return merge_int_g_exprs(sym_expr, base_kernel=self.base_kernel) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, @@ -383,14 +408,14 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, extra_deriv_dirs=[]): stokeslet_obj = StokesletWrapper(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu) + mu_sym=self.mu, nu_sym=self.nu, method=self.method) sym_expr = self.apply(stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stresslet_weight sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight - return sym_expr + return merge_int_g_exprs(sym_expr) def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, @@ -453,7 +478,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, - extra_deriv_dirs): + extra_deriv_dirs=[]): sym_expr = np.zeros((self.dim,), dtype=object) @@ -531,7 +556,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): - stresslet = StressletWrapperTornberg(dim=3, self.mu, self.nu) + stresslet = StressletWrapperTornberg(3, self.mu, self.nu) return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) @@ -585,18 +610,13 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): mu_sym=mu_sym, nu_sym=nu_sym) elif method == "biharmonic" or method == "naive": self.stresslet = StressletWrapper(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu_sym=mu_sym, nu_sym=nu_sym, method=method) self.stokeslet = StokesletWrapper(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu_sym=mu_sym, nu_sym=nu_sym, method=method) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") - if method == "biharmonic": - self.base_kernel = BiharmonicKernel(dim=ambient_dim) - else: - self.base_kernel = None - @property def dim(self): return self.ambient_dim - 1 @@ -707,20 +727,18 @@ def _operator(self, sigma, normal, qbx_forced_limit): return result def prepare_rhs(self, b): - return merge_int_g_exprs(b + self._farfield(qbx_forced_limit=+1), - base_kernel=self.base_kernel) + return b + self._farfield(qbx_forced_limit=+1) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. K. 1985 Equation 2.18 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator( - sigma, normal, qbx_forced_limit), base_kernel=self.base_kernel) + sigma, normal, qbx_forced_limit)) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 return merge_int_g_exprs( -self._farfield(qbx_forced_limit) - - self._operator(sigma, normal, qbx_forced_limit), - base_kernel=self.base_kernel) + - self._operator(sigma, normal, qbx_forced_limit)) def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: H. K. 1985 Equation 2.17 @@ -759,17 +777,17 @@ def __init__(self, *, eta=None, method="laplace", mu_sym=var("mu"), nu_sym=0.5): def _operator(self, sigma, normal, qbx_forced_limit): return self.stresslet.apply_stokeslet_and_stresslet(sigma, - sigma, normal, qbx_forced_limit, self.eta, 1) + sigma, normal, qbx_forced_limit, self.eta, 1, []) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, - normal, qbx_forced_limit), base_kernel=self.base_kernel, verbose=True) + normal, qbx_forced_limit)) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 return merge_int_g_exprs(-self._operator(sigma, normal, - qbx_forced_limit), base_kernel=self.base_kernel) + qbx_forced_limit)) def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the diff --git a/test/test_stokes.py b/test/test_stokes.py index 098add093..185fe6052 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -161,9 +161,7 @@ def run_exterior_stokes(ctx_factory, *, sym_velocity = op.velocity(sym_sigma, normal=sym_normal) - from pytential.symbolic.stokes import StokesletWrapper - sym_source_pot = StokesletWrapper(ambient_dim, - nu_sym=sym_nu).apply(sym_sigma, qbx_forced_limit=None) + sym_source_pot = op.stokeslet.apply(sym_sigma, qbx_forced_limit=None) # }}} From 30a176dfe35204326d6cf4c5b36441c5191b417a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 17:48:12 -0500 Subject: [PATCH 062/209] Fix laplace --- pytential/symbolic/elasticity.py | 6 +++--- pytential/symbolic/stokes.py | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index e8ca67f80..2f42eb8e5 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -302,6 +302,7 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, mu = self.mu nu = self.nu lam = 2*nu*mu/(1-2*nu) + stokeslet_weight *= -1 def C(i, j, k, l): # noqa: E741 res = 0 @@ -340,7 +341,6 @@ def Q(i, int_g): sigma = stresslet_density_vec_sym source_kernels = [None]*4 - densities = [0]*4 for i in range(3): source_kernels[i] = AxisSourceDerivative(i, kernel) source_kernels[3] = kernel @@ -359,6 +359,7 @@ def Q(i, int_g): qbx_forced_limit=qbx_forced_limit) sym_expr[i] += P(i, k, int_g) + densities = [0]*4 for k in range(3): for m in range(3): for j in range(3): @@ -398,8 +399,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=[]): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): stresslet = StressletWrapperYoshida(3, self.mu, self.nu) return self.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 483e46eeb..3b210d67a 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -75,7 +75,8 @@ def __init__(self, dim, mu_sym, nu_sym): self.nu = nu_sym args = list(get_all_variables(self.mu)) + list(get_all_variables(self.nu)) - self.extra_kwargs = {arg.name: arg for arg in args} + #self.extra_kwargs = {arg.name: arg for arg in args} + self.extra_kwargs = {} def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stokeslet kernel. @@ -175,7 +176,8 @@ def __init__(self, dim, mu_sym, nu_sym): self.nu = nu_sym args = list(get_all_variables(self.nu)) - self.extra_kwargs = {arg.name: arg for arg in args} + #self.extra_kwargs = {arg.name: arg for arg in args} + self.extra_kwargs = {} def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stresslet kernel. @@ -279,10 +281,11 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, Returns the Integral of the Stokeslet/Stresslet kernel given by `idx` and its derivatives. """ - return _create_int_g(self.kernel_dict[idx], deriv_dirs, + res = _create_int_g(self.kernel_dict[idx], deriv_dirs, density=density_sym*dir_vec_sym[idx[-1]], qbx_forced_limit=qbx_forced_limit, **self.extra_kwargs)/(2*(1-self.nu)) + return res def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): @@ -487,8 +490,8 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, k in range(self.dim)] common_source_kernels.append(self.kernel) - stokeslet_weight *= 3.0/6 - stresslet_weight *= -0.5*self.mu**(-1) + stresslet_weight *= 3.0/6 + stokeslet_weight *= -0.5*self.mu**(-1) for i in range(self.dim): for j in range(self.dim): @@ -521,7 +524,7 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, densities = [stresslet_weight*(common_density0 * dir_vec_sym[k] + common_density1 * stresslet_density_vec_sym[k]) for k in range(self.dim)] - densities.append(common_density2) + densities.append(stokeslet_weight * common_density2) target_kernel = AxisTargetDerivative(i, self.kernel) for deriv_dir in extra_deriv_dirs: @@ -554,8 +557,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=[]): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): stresslet = StressletWrapperTornberg(3, self.mu, self.nu) return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, From 25b0b95ebe36ce5cd27aeb0fe61a38e245b26647 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 10 Jun 2021 18:17:48 -0500 Subject: [PATCH 063/209] Don't use the kernel_arguments from kernel for base_kernel --- pytential/symbolic/pde/system_utils.py | 6 +++--- pytential/symbolic/primitives.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index f5782830f..97ce41dab 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -245,7 +245,7 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): knl = AxisSourceDerivative(d, knl) c *= -1 result += int_g.copy(target_kernel=base_kernel, source_kernels=(knl,), - densities=(density,)) * c + densities=(density,), kernel_arguments=None) * c return result @@ -283,7 +283,7 @@ def _convert_target_multiplier_to_source(int_g): new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) for density in int_g.densities] result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, - densities=tuple(new_densities))) + densities=tuple(new_densities), kernel_arguments=None)) # create a new expression kernel for (x - y)*G sym_d = make_sym_vector("d", tgt_knl.dim) @@ -297,7 +297,7 @@ def _convert_target_multiplier_to_source(int_g): knl.is_complex_valued) result.append(int_g.copy(target_kernel=new_knl, densities=(density,), - source_kernels=(new_knl,))) + source_kernels=(new_knl,), kernel_arguments=None)) return result diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 594bbf6f4..44a30ddb5 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1550,7 +1550,7 @@ def __init__(self, target_kernel, source_kernels, densities, def copy(self, target_kernel=None, densities=None, qbx_forced_limit=_NoArgSentinel, source=None, target=None, - kernel_arguments=None, source_kernels=None): + kernel_arguments=_NoArgSentinel, source_kernels=None): target_kernel = target_kernel or self.target_kernel source_kernels = source_kernels or self.source_kernels densities = densities or self.densities @@ -1558,7 +1558,8 @@ def copy(self, target_kernel=None, densities=None, qbx_forced_limit = self.qbx_forced_limit source = as_dofdesc(source or self.source) target = as_dofdesc(target or self.target) - kernel_arguments = kernel_arguments or self.kernel_arguments + if kernel_arguments is _NoArgSentinel: + kernel_arguments = self.kernel_arguments return type(self)(target_kernel, source_kernels, densities, qbx_forced_limit, source, target, kernel_arguments) From 1419415d335e1acc7f62cadb5dc2d847e30c67dc Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 11 Jun 2021 12:54:59 -0500 Subject: [PATCH 064/209] WIP --- test/test_elasticity2.py | 250 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 test/test_elasticity2.py diff --git a/test/test_elasticity2.py b/test/test_elasticity2.py new file mode 100644 index 000000000..f17b99fa1 --- /dev/null +++ b/test/test_elasticity2.py @@ -0,0 +1,250 @@ +__copyright__ = "Copyright (C) 2017 Natalie Beams" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import numpy as np +import pyopencl as cl + +from arraycontext import PyOpenCLArrayContext +from meshmode.discretization import Discretization +from meshmode.discretization.poly_element import \ + InterpolatoryQuadratureSimplexGroupFactory + +from pytools.obj_array import make_obj_array + +from pytential import bind, sym +from pytential import GeometryCollection + +import pytest +from pyopencl.tools import ( # noqa + pytest_generate_tests_for_pyopencl + as pytest_generate_tests) + +import logging +logger = logging.getLogger(__name__) + + +# {{{ test_exterior_stokes + +def run_exterior_stokes(actx, *, + ambient_dim, target_order, qbx_order, resolution, + fmm_order=None, # FIXME: FMM is slower than direct evaluation + source_ovsmp=None, + radius=1.5, + mu=1.0, + nu=0.4, + verbose=False, + method="naive", + + _target_association_tolerance=0.05, + _expansions_in_tree_have_extent=True): + # {{{ geometry + + if source_ovsmp is None: + source_ovsmp = 4 if ambient_dim == 2 else 8 + + places = {} + + if ambient_dim == 3: + from meshmode.mesh.generation import generate_surface_of_revolution + nheight = 10*(resolution + 1) + nangle = 10*(resolution + 1) + mesh = generate_surface_of_revolution( + lambda x, y: np.ones(x.shape), + np.linspace(-1.5, -0.5, nheight), + np.linspace(0, 2*np.pi, nangle, endpoint=False), target_order) + else: + raise ValueError(f"unsupported dimension: {ambient_dim}") + + mesh_vertices_image = mesh.vertices.copy() + mesh_vertices_image[2, :] *= -1 + mesh_grp, = mesh.groups + mesh_grp_nodes_image = mesh_grp.nodes.copy() + mesh_grp_nodes_image[2, :] *= -1 + mesh_grp_image = mesh_grp.copy(nodes=mesh_grp_nodes_image) + mesh_image = mesh.copy(vertices=mesh_vertices_image, groups=[mesh_grp_image]) + + pre_density_discr = Discretization(actx, mesh, + InterpolatoryQuadratureSimplexGroupFactory(target_order)) + pre_density_discr_image = pre_density_discr.copy(mesh=mesh_image) + + from pytential.qbx import QBXLayerPotentialSource + qbx = QBXLayerPotentialSource(pre_density_discr, + fine_order=source_ovsmp * target_order, + qbx_order=qbx_order, + fmm_order=fmm_order, + _max_leaf_refine_weight=64, + target_association_tolerance=_target_association_tolerance, + _expansions_in_tree_have_extent=_expansions_in_tree_have_extent) + places["source"] = qbx + places["target"] = qbx + places["target_image"] = qbx.copy(density_discr=pre_density_discr_image) + + from extra_int_eq_data import make_source_and_target_points + point_source, point_target = make_source_and_target_points( + actx, + side=+1, + inner_radius=0.5 * radius, + outer_radius=2.0 * radius, + ambient_dim=ambient_dim, + ) + places["point_source"] = point_source + places["point_target"] = point_target + + places = GeometryCollection(places, auto_where="source") + + density_discr = places.get_discretization("source") + logger.info("ndofs: %d", density_discr.ndofs) + logger.info("nelements: %d", density_discr.mesh.nelements) + + # }}} + + # {{{ symbolic + + sym_normal = sym.make_sym_vector("normal", ambient_dim) + sym_mu = sym.var("mu") + sym_nu = sym.var("nu") + + if ambient_dim == 3: + from pytential.symbolic.elasticity import MindlinOperator + op = MindlinOperator(method=method, mu_sym=sym_mu, nu_sym=sym_nu) + else: + raise AssertionError() + + sym_sigma = op.get_density_var("sigma") + sym_bc = op.get_density_var("bc") + sym_rhs = sym_bc + sym_source_pot = op.free_space_op.stokeslet.apply(sym_sigma, qbx_forced_limit=None) + + # }}} + + # {{{ boundary conditions + + normal = bind(places, sym.normal(ambient_dim).as_vector())(actx) + + np.random.seed(42) + charges = make_obj_array([ + actx.from_numpy(np.random.randn(point_source.ndofs)) + for _ in range(ambient_dim) + ]) + + bc_context = {"nu": nu} + op_context = {"mu": mu, "normal": normal, "nu": nu} + direct_context = {"mu": mu, "nu": nu} + + bc_op = bind(places, sym_source_pot, + auto_where=("point_source", "source")) + bc = bc_op(actx, sigma=charges, **direct_context) + + rhs = bind(places, sym_rhs)(actx, bc=bc, **bc_context) + + sym_op = op.free_space_operator(sym_sigma, normal=sym_normal, qbx_forced_limit=1) + sym_op_image = op.operator(sym_sigma, normal=sym_normal, qbx_forced_limit=2) + bound_op = bind(places, sym_op, auto_where=("source", "target")) + bound_op_image = bind(places, sym_op_image, auto_where=("source", "target_image")) + # }}} + + def print_timing_data(timings, name): + result = {k: 0 for k in list(timings.values())[0].keys()} + total = 0 + for k, timing in timings.items(): + for k, v in timing.items(): + result[k] += v['wall_elapsed'] + total += v['wall_elapsed'] + result['total'] = total + print(f"{name}={result}") + + fmm_timing_data = {} + v1 = bound_op.eval({"sigma": rhs, **op_context}, array_context=actx, + timing_data=fmm_timing_data) + print_timing_data(fmm_timing_data, method) + + fmm_timing_data = {} + v2 = bound_op_image.eval({"sigma": rhs, **op_context}, array_context=actx, + timing_data=fmm_timing_data) + print_timing_data(fmm_timing_data, method) + + h_max = bind(places, sym.h_max(ambient_dim))(actx) + + return h_max, v1 + v2 + + +@pytest.mark.parametrize("method, nu", [ + ("biharmonic", 0.4), + ("laplace", 0.4), + ]) +def test_exterior_stokes(ctx_factory, method, nu, verbose=False): + if verbose: + logging.basicConfig(level=logging.INFO) + + def rnorm2(x, y): + y_norm = actx.np.linalg.norm(y.dot(y), ord=2) + if y_norm < 1.0e-14: + y_norm = 1.0 + + d = x - y + return actx.np.linalg.norm(d.dot(d), ord=2) / y_norm + + target_order = 3 + qbx_order = 3 + cl_ctx = cl.create_some_context() + queue = cl.CommandQueue(cl_ctx, + properties=cl.command_queue_properties.PROFILING_ENABLE) + actx = PyOpenCLArrayContext(queue) + fmm_order = 6 + resolution = 0 + ambient_dim = 3 + + kwargs = dict( + actx=actx, + ambient_dim=ambient_dim, + target_order=target_order, + qbx_order=qbx_order, + resolution=resolution, + verbose=verbose, + nu=nu, + method=method, + ) + + h_max, fmm_result = run_exterior_stokes(fmm_order=fmm_order, **kwargs) + _, direct_result = run_exterior_stokes(fmm_order=False, **kwargs) + + v_error = rnorm2(fmm_result, direct_result) + print(v_error) + + assert v_error < 1e-5 + + +# }}} + + +# You can test individual routines by typing +# $ python test_stokes.py 'test_routine()' + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + exec(sys.argv[1]) + else: + from pytest import main + main([__file__]) + +# vim: fdm=marker From 09699dc768957067d45eef0e03ee7bbd56a3815e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 11 Jun 2021 16:22:26 -0500 Subject: [PATCH 065/209] fix tests --- pytential/symbolic/elasticity.py | 10 ++++------ pytential/symbolic/pde/system_utils.py | 20 +++++++++++++++++--- pytential/symbolic/primitives.py | 3 ++- pytential/symbolic/stokes.py | 23 ++++++----------------- pytential/unregularized.py | 1 + 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 2f42eb8e5..9288f95c1 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -57,9 +57,8 @@ def __init__(self, method="laplace", mu_sym=var("mu"), nu_sym=0.5): self.laplace_kernel = LaplaceKernel(3) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): - return merge_int_g_exprs(self.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit), - base_kernel=self.base_kernel) + return self.stresslet.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit) def get_density_var(self, name="sigma"): """ @@ -256,7 +255,7 @@ def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # TODO: make merge_int_g_exprs smart enough to merge two different # kernels into two separate IntGs. result = merge_int_g_exprs(resultA + resultC, - base_kernel=self.free_space_op.base_kernel) + base_kernel=self.free_space_op.stresslet.base_kernel) result += merge_int_g_exprs(resultB, base_kernel=self.compression_knl) return result else: @@ -401,7 +400,6 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): stresslet = StressletWrapperYoshida(3, self.mu, self.nu) - return self.apply_stokeslet_and_stresslet(density_vec_sym, + return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) - diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 97ce41dab..83f3c153a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -213,6 +213,16 @@ def convert_int_g_to_base(int_g, base_kernel, verbose=False): return result +def _filter_kernel_arguments(knls, kernel_arguments): + kernel_arg_names = set() + + for kernel in knls: + for karg in (kernel.get_args() + kernel.get_source_args()): + kernel_arg_names.add(karg.loopy_arg.name) + + return {k: v for (k, v) in kernel_arguments.items() if k in kernel_arg_names} + + def _convert_int_g_to_base(int_g, base_kernel, verbose=False): tgt_knl = int_g.target_kernel dim = tgt_knl.dim @@ -238,6 +248,8 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): if source_kernel == source_kernel.get_base_kernel(): result += const + new_kernel_args = _filter_kernel_arguments([base_kernel], int_g.kernel_arguments) + for mi, c in deriv_relation[1]: knl = source_kernel.replace_base_kernel(base_kernel) for d, val in enumerate(mi): @@ -245,7 +257,7 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): knl = AxisSourceDerivative(d, knl) c *= -1 result += int_g.copy(target_kernel=base_kernel, source_kernels=(knl,), - densities=(density,), kernel_arguments=None) * c + densities=(density,), kernel_arguments=new_kernel_args) * c return result @@ -277,13 +289,15 @@ def _convert_target_multiplier_to_source(int_g): return int_g if isinstance(tgt_knl.inner_kernel, KernelWrapper): return int_g + + new_kernel_args = _filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) result = [] # x G = y*G + (x - y)*G # For y*G, absorb y into a density new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) for density in int_g.densities] result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, - densities=tuple(new_densities), kernel_arguments=None)) + densities=tuple(new_densities), kernel_arguments=new_kernel_args)) # create a new expression kernel for (x - y)*G sym_d = make_sym_vector("d", tgt_knl.dim) @@ -297,7 +311,7 @@ def _convert_target_multiplier_to_source(int_g): knl.is_complex_valued) result.append(int_g.copy(target_kernel=new_knl, densities=(density,), - source_kernels=(new_knl,), kernel_arguments=None)) + source_kernels=(new_knl,), kernel_arguments=new_kernel_args)) return result diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 44a30ddb5..8cb071b26 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1503,7 +1503,7 @@ def __init__(self, target_kernel, source_kernels, densities, % qbx_forced_limit) densities = list(densities) - source_kernels = tuple([source_kernels[i] for i in range(len(densities)) \ + source_kernels = tuple([source_kernels[i] for i in range(len(densities)) if densities[i] != 0]) densities = tuple([density for density in densities if density != 0]) @@ -1531,6 +1531,7 @@ def __init__(self, target_kernel, source_kernels, densities, provided_arg_names = set(kernel_arguments.keys()) missing_args = kernel_arg_names - provided_arg_names + if missing_args: raise TypeError("kernel argument(s) '%s' not supplied" % ", ".join(missing_args)) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 3b210d67a..6cdd94ba7 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,7 +27,6 @@ from sumpy.kernel import (StressletKernel, LaplaceKernel, ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) -from sumpy.tools import get_all_variables from pymbolic import var __doc__ = """ @@ -74,10 +73,6 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym - args = list(get_all_variables(self.mu)) + list(get_all_variables(self.nu)) - #self.extra_kwargs = {arg.name: arg for arg in args} - self.extra_kwargs = {} - def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): """Symbolic expressions for integrating Stokeslet kernel. @@ -175,11 +170,8 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym - args = list(get_all_variables(self.nu)) - #self.extra_kwargs = {arg.name: arg for arg in args} - self.extra_kwargs = {} - - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=[]): """Symbolic expressions for integrating Stresslet kernel. Returns an object array of symbolic expressions for the vector @@ -283,8 +275,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, """ res = _create_int_g(self.kernel_dict[idx], deriv_dirs, density=density_sym*dir_vec_sym[idx[-1]], - qbx_forced_limit=qbx_forced_limit, - **self.extra_kwargs)/(2*(1-self.nu)) + qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) return res def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): @@ -387,8 +378,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, knl = self.kernel_dict[kernel_idx] result += _create_int_g(knl, deriv_dirs + extra_deriv_dirs, density=density_sym*dir_vec_sym[dir_vec_idx], - qbx_forced_limit=qbx_forced_limit, - **self.extra_kwargs) * coeff + qbx_forced_limit=qbx_forced_limit) * coeff return result/(2*(1 - nu)) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, @@ -420,7 +410,6 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, return merge_int_g_exprs(sym_expr) - def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -497,7 +486,7 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, for j in range(self.dim): densities = [stresslet_weight*( stresslet_density_vec_sym[k] * dir_vec_sym[j] - + stresslet_density_vec_sym[j] * dir_vec_sym[k]) \ + + stresslet_density_vec_sym[j] * dir_vec_sym[k]) for k in range(self.dim)] densities.append(stokeslet_weight*stokeslet_density_vec_sym[j]) target_kernel = TargetPointMultiplier(j, @@ -528,7 +517,7 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, target_kernel = AxisTargetDerivative(i, self.kernel) for deriv_dir in extra_deriv_dirs: - target_kernel = AxisTargetDerivative(deriv_dir, targer_kernel) + target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=tuple(common_source_kernels), densities=tuple(densities), diff --git a/pytential/unregularized.py b/pytential/unregularized.py index 80ff84f25..c784945ac 100644 --- a/pytential/unregularized.py +++ b/pytential/unregularized.py @@ -120,6 +120,7 @@ def evaluate_wrapper(expr): def op_group_features(self, expr): from pytential.utils import sort_arrays_together + from sumpy.kernel import TargetTransformationRemover result = ( expr.source, *sort_arrays_together(expr.source_kernels, expr.densities, key=str), From 299c8bbb50074726758246adb62226599a5a2c96 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 18 Jun 2021 17:57:24 -0500 Subject: [PATCH 066/209] Fix 2D tests --- pytential/symbolic/stokes.py | 21 ++++++++++++++------- test/test_stokes.py | 9 ++++++++- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 6cdd94ba7..be06f53ea 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -403,9 +403,12 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stokeslet_obj = StokesletWrapper(dim=self.dim, mu_sym=self.mu, nu_sym=self.nu, method=self.method) - sym_expr = self.apply(stresslet_density_vec_sym, dir_vec_sym, + sym_expr = 0 + if stresslet_weight != 0: + sym_expr += self.apply(stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stresslet_weight - sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, + if stokeslet_weight != 0: + sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight return merge_int_g_exprs(sym_expr) @@ -693,9 +696,10 @@ def __init__(self, *, omega, alpha=None, eta=None, method="biharmonic", def _farfield(self, qbx_forced_limit): source_dofdesc = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) length = sym.integral(self.ambient_dim, self.dim, 1, dofdesc=source_dofdesc) - return self.stresslet.apply( - -self.omega / length, [0]*self.dim, [0]*self.dim, - qbx_forced_limit, 1, 0) + return self.stresslet.apply_stokeslet_and_stresslet( + -self.omega / length, [0]*self.ambient_dim, [0]*self.ambient_dim, + qbx_forced_limit=qbx_forced_limit, stokeslet_weight=1, + stresslet_weight=0) def _operator(self, sigma, normal, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit @@ -713,7 +717,8 @@ def _operator(self, sigma, normal, qbx_forced_limit): result = self.eta * self.alpha / (2.0 * np.pi) * int_sigma result += self.stresslet.apply_stokeslet_and_stresslet(meanless_sigma, - sigma, normal, qbx_forced_limit, -self.eta, 1) + sigma, normal, qbx_forced_limit=qbx_forced_limit, + stokeslet_weight=-self.eta, stresslet_weight=1) return result @@ -768,7 +773,9 @@ def __init__(self, *, eta=None, method="laplace", mu_sym=var("mu"), nu_sym=0.5): def _operator(self, sigma, normal, qbx_forced_limit): return self.stresslet.apply_stokeslet_and_stresslet(sigma, - sigma, normal, qbx_forced_limit, self.eta, 1, []) + sigma, normal, qbx_forced_limit=qbx_forced_limit, + stokeslet_weight=self.eta, stresslet_weight=1, + extra_deriv_dirs=[]) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 diff --git a/test/test_stokes.py b/test/test_stokes.py index 185fe6052..5eacfd370 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -161,7 +161,14 @@ def run_exterior_stokes(ctx_factory, *, sym_velocity = op.velocity(sym_sigma, normal=sym_normal) - sym_source_pot = op.stokeslet.apply(sym_sigma, qbx_forced_limit=None) + if ambient_dim == 3: + sym_source_pot = op.stokeslet.apply(sym_sigma, qbx_forced_limit=None) + else: + # Use the naive method here as biharmonic requires source derivatives + # of point_source + from pytential.symbolic.stokes import StokesletWrapper + sym_source_pot = StokesletWrapper(ambient_dim, mu_sym=sym_nu, + nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) # }}} From 123cd79bcf81a52b785c14d6fd0b6706a5a8130e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 20 Jun 2021 10:37:37 -0500 Subject: [PATCH 067/209] WIP reduce number of fmms --- pytential/symbolic/pde/system_utils.py | 313 +++++++++++++++++++++++-- 1 file changed, 287 insertions(+), 26 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 83f3c153a..58dc5ee65 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -22,7 +22,7 @@ import numpy as np -from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper +from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper, sqrt from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, DirectionalSourceDerivative, ExpressionKernel, KernelWrapper, TargetPointMultiplier) @@ -30,11 +30,12 @@ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) -from pymbolic.mapper import WalkMapper -from pymbolic.primitives import Sum, Product, Quotient +from pymbolic.mapper import WalkMapper, Mapper +from pymbolic.primitives import Sum, Product, Quotient, Power from pytential.symbolic.primitives import IntG, NodeCoordinateComponent, int_g_vec import pytential +from collections import defaultdict def _chop(expr, tol): nums = expr.atoms(sym.Number) @@ -261,7 +262,7 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): return result -def merge_int_g_exprs(exprs, base_kernel=None, verbose=False): +def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, source_dependent_variables=None): replacements = {} if base_kernel is not None: @@ -279,7 +280,272 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False): replacements[int_g] = sum(convert_int_g_to_base(new_int_g, base_kernel, verbose=verbose) for new_int_g in new_int_g_s) - return np.array([merge_int_g_expr(expr, replacements) for expr in exprs]) + result_coeffs = [] + result_int_gs = [] + + for expr in exprs: + if not have_int_g_s(expr): + result_coeffs.append(expr) + result_int_gs.append(0) + try: + result_coeff, result_int_g = _merge_int_g_expr(expr, replacements) + result_int_g = _convert_axis_source_to_directional_source(result_int_g) + result_int_g = result_int_g.copy( + densities=_simplify_densities(result_int_g.densities)) + result_coeffs.append(result_coeff) + result_int_gs.append(result_int_g) + except AssertionError: + result_coeffs.append(expr) + result_int_gs.append(0) + + if source_dependent_variables is not None: + result_int_gs = _reduce_number_of_fmms(result_int_gs, source_dependent_variables) + result = [coeff + int_g for coeff, int_g in zip(result_coeffs, result_int_gs)] + return np.array(result, dtype=object) + + +def _convert_kernel_to_poly(kernel, axis_vars): + if isinstance(kernel, AxisTargetDerivative): + poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) + return axis_vars[kernel.axis]*poly + elif isinstance(kernel, AxisSourceDerivative): + poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) + return -axis_vars[kernel.axis]*poly + return 1 + + +def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): + from pymbolic.interop.sympy import SympyToPymbolicMapper + to_pymbolic = SympyToPymbolicMapper() + + orig_kernel = orig_int_g.source_kernels[0] + source_kernels = [] + densities = [] + for monom, coeff in poly.terms(): + kernel = orig_kernel + for idim, rep in enumerate(monom): + for _ in range(rep): + kernel = AxisSourceDerivative(idim, kernel) + source_kernels.append(kernel) + densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) + return orig_int_g.copy(source_kernels=tuple(source_kernels), + densities=tuple(densities)) + + +def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): + from pymbolic.interop.sympy import SympyToPymbolicMapper + to_pymbolic = SympyToPymbolicMapper() + + result = 0 + for monom, coeff in poly.terms(): + kernel = orig_int_g.target_kernel + for idim, rep in enumerate(monom): + for _ in range(rep): + kernel = AxisTargetDerivative(idim, kernel) + result += orig_int_g.copy(target_kernel=kernel, + source_kernels=rhs_int_g.source_kernels, + densities=rhs_int_g.densities) * to_pymbolic(coeff) + + return result + + +def _syzygy_module(m, gens): + import sympy + from sympy.polys.orderings import grevlex + + def _convert_to_matrix(module, *gens): + import sympy + result = [] + for syzygy in module: + row = [] + for dmp in syzygy.data: + row.append(sympy.Poly(dmp.to_dict(), *gens, domain=sympy.EX).as_expr()) + result.append(row) + return sympy.Matrix(result) + + ring = sympy.EX.old_poly_ring(*gens, order=grevlex) + column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist(), order=grevlex) + for i in range(m.shape[1])] + column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] + + intersection = column_syzygy_modules[0] + for i in range(1, len(column_syzygy_modules)): + intersection = intersection.intersect(column_syzygy_modules[i]) + + m2 = intersection._groebner_vec() + m3 = _convert_to_matrix(m2, *gens) + return m3 + + +def _factor_left(mat, axis_vars): + return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T + + +def _factor_right(mat, factor_left): + import sympy + ys = sympy.symbols(f"_y{{0:{factor_left.shape[1]}}}") + factor_right = [] + for i in range(mat.shape[1]): + aug_mat = sympy.zeros(factor_left.shape[0], factor_left.shape[1] + 1) + aug_mat[:, :factor_left.shape[1]] = factor_left + aug_mat[:, factor_left.shape[1]] = mat[:, i] + res_map = sympy.solve_linear_system(aug_mat, *ys) + row = [] + for y in ys: + row.append(res_map[y]) + factor_right.append(row) + factor_right = sympy.Matrix(factor_right).T + return factor_right + + +def _check_int_gs_common(int_gs): + base_kernel = int_gs[0].source_kernels[0].get_base_kernel() + common_int_g = int_gs[0].copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)) + for int_g in int_gs: + for source_kernel in int_g.source_kernels: + if source_kernel.get_base_kernel() != base_kernel: + return False + if common_int_g != int_g.copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)): + return False + return True + + +def _reduce_number_of_fmms(int_gs, source_dependent_variables): + from pymbolic.interop.sympy import SympyToPymbolicMapper, PymbolicToSympyMapper + import sympy + + source_exprs = [] + mapper = ConvertDensityToSourceExprCoeffMap(source_dependent_variables) + matrix = [] + dim = int_gs[0].target_kernel.dim + axis_vars = sympy.symbols(f"_x0:{dim}") + to_sympy = PymbolicToSympyMapper() + to_pymbolic = SympyToPymbolicMapper() + + _check_int_gs_common(int_gs) + + for int_g in int_gs: + row = [0]*len(source_exprs) + for density, source_kernel in zip(int_g.densities, int_g.source_kernels): + try: + d = mapper(density) + except ImportError: + return int_gs + for source_expr, coeff in d.items(): + if source_expr not in source_exprs: + source_exprs.append(source_expr) + row += [0] + poly = _convert_kernel_to_poly(source_kernel, axis_vars) + row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) + matrix.append(row) + + for row in matrix: + row += [0]*(len(source_exprs) - len(row)) + + mat = sympy.Matrix(matrix) + lhs_mat = _factor_left(mat, axis_vars) + rhs_mat = _factor_right(mat, lhs_mat) + + if rhs_mat.shape[0] >= mat.shape[0]: + return int_gs + + rhs_mat = rhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) + lhs_mat = lhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) + + rhs_polys = [0] * rhs_mat.shape[0] + base_kernel = int_gs[0].source_kernels[0].get_base_kernel() + base_int_g = int_gs[0].copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)) + rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) \ + for poly in row] for row in rhs_mat.tolist()] + + rhs_int_gs = [] + for i in range(rhs_mat.shape[0]): + source_kernels = [] + densities = [] + for j in range(rhs_mat.shape[1]): + new_densities = [density * source_expr[j] for density in \ + rhs_mat_int_gs[i][j].densities] + source_kernels.extend(rhs_mat_int_gs[i][j].source_kernels) + densities.extend(new_densities) + rhs_int_gs.append(rhs_mat_int_gs[i][0].copy( + source_kernels=tuple(source_kernels), densities=tuple(densities))) + + res = [0]*lhs_mat.shape[0] + for i in range(lhs_mat.shape[0]): + for j in range(lhs_mat.shape[1]): + res[i] += _convert_target_poly_to_int_g(lhs_mat[i, j], + int_gs[i], rhs_int_gs[j]) + + return res + + +class ConvertDensityToSourceExprCoeffMap(Mapper): + def __init__(self, source_dependent_variables): + self.source_dependent_variables = source_dependent_variables + + def __call__(self, expr): + if expr in self.source_dependent_variables: + return {expr: 1} + try: + return super().__call__(expr) + except NotImplementedError: + return {1: expr} + + rec = __call__ + + def map_sum(self, expr): + d = defaultdict(lambda: 0) + for child in expr.children: + d_child = self.rec(child) + for k, v in d_child.items(): + d[k] += v + return dict(d) + + def map_product(self, expr): + if len(expr.children) > 2: + left = Product(tuple(expr.children[:2])) + right = Product(tuple(expr.children[2:])) + new_prod = Product((left, right)) + return self.rec(new_prod) + elif len(expr.children) == 1: + return self.rec(expr.children[0]) + elif len(expr.children) == 0: + return {1: 1} + left, right = expr.children + d_left = self.rec(left) + d_right = self.rec(right) + d = defaultdict(lambda : 0) + for k_left, v_left in d_left.items(): + for k_right, v_right in d_right.items(): + d[k_left*k_right] += v_left*v_right + return dict(d) + + def map_power(self, expr): + d_base = self.rec(expr.base) + d_exponent = self.rec(expr.exponent) + if len(d_exponent) > 1: + raise ValueError + exp_k, exp_v = list(d_exponent.items())[0] + if exp_k != 1: + raise ValueError + for k in d_base.keys(): + d_base[k] = d_base[k]**exp_v + return d_base + + def map_quotient(self, expr): + d_num = self.rec(expr.numerator) + d_den = self.rec(expr.denominator) + if len(d_den) > 1: + raise ValueError + den_k, den_v = list(d_den.items())[0] + if den_k != 1: + raise ValueError + for k in d_num.keys(): + d_num[k] /= den_v + return d_num def _convert_target_multiplier_to_source(int_g): @@ -373,19 +639,6 @@ def _convert_axis_source_to_directional_source(int_g): return res -def merge_int_g_expr(expr, replacements): - if not have_int_g_s(expr): - return expr - try: - result_coeff, result_int_g = _merge_int_g_expr(expr, replacements) - result_int_g = _convert_axis_source_to_directional_source(result_int_g) - result_int_g = result_int_g.copy( - densities=_simplify_densities(result_int_g.densities)) - return result_coeff + result_int_g - except AssertionError: - return expr - - def _merge_source_kernel_duplicates(source_kernels, densities): new_source_kernels = [] new_densities = [] @@ -487,13 +740,21 @@ def _merge_int_g_expr(expr, replacements): from sumpy.kernel import (StokesletKernel, BiharmonicKernel, StressletKernel, ElasticityKernel, LaplaceKernel) base_kernel = BiharmonicKernel(3) - kernels = [StokesletKernel(3, 0, 1), StokesletKernel(3, 0, 0)] - kernels += [StressletKernel(3, 0, 1, 0), StressletKernel(3, 0, 0, 0), - StressletKernel(3, 0, 1, 2)] - kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), - ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] - get_deriv_relation(kernels, base_kernel, tol=1e-10, order=2, verbose=True) - density = pytential.sym.make_sym_vector("d", 1)[0] + #base_kernel = LaplaceKernel(3) + kernels = [StokesletKernel(3, 0, 2), StokesletKernel(3, 0, 0)] + kernels += [StressletKernel(3, 0, 0, 0), StressletKernel(3, 0, 0, 1), + StressletKernel(3, 0, 0, 2), StressletKernel(3, 0, 1, 2)] + + sym_d = make_sym_vector("d", base_kernel.dim) + sym_r = sqrt(sum(a**2 for a in sym_d)) + conv = SympyToPymbolicMapper() + expression_knl = ExpressionKernel(3, conv(sym_d[0]*sym_d[1]/sym_r**3), 1, False) + expression_knl2 = ExpressionKernel(3, conv(1/sym_r + sym_d[0]*sym_d[0]/sym_r**3), 1, False) + kernels = [expression_knl, expression_knl2] + #kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), + # ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=True) + """density = pytential.sym.make_sym_vector("d", 1)[0] int_g_1 = int_g_vec(TargetPointMultiplier(2, AxisTargetDerivative(2, AxisSourceDerivative(1, AxisSourceDerivative(0, LaplaceKernel(3))))), density, qbx_forced_limit=1) @@ -501,4 +762,4 @@ def _merge_int_g_expr(expr, replacements): AxisSourceDerivative(0, AxisSourceDerivative(0, LaplaceKernel(3))))), density, qbx_forced_limit=1) print(merge_int_g_exprs([int_g_1, int_g_2], - base_kernel=BiharmonicKernel(3), verbose=True)[0]) + base_kernel=BiharmonicKernel(3), verbose=True)[0])""" From 7ee639cef88bcbe38e7f59786553b3a4c4d53c18 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 20 Jun 2021 20:47:01 -0500 Subject: [PATCH 068/209] Fix source_expr typo and fix formatting --- pytential/symbolic/pde/system_utils.py | 44 ++++++++++++++------------ 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 58dc5ee65..04334ae6a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -31,12 +31,13 @@ as gnitstam) from pymbolic.mapper import WalkMapper, Mapper -from pymbolic.primitives import Sum, Product, Quotient, Power -from pytential.symbolic.primitives import IntG, NodeCoordinateComponent, int_g_vec +from pymbolic.primitives import Sum, Product, Quotient +from pytential.symbolic.primitives import IntG, NodeCoordinateComponent import pytential from collections import defaultdict + def _chop(expr, tol): nums = expr.atoms(sym.Number) replace_dict = {} @@ -262,7 +263,8 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): return result -def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, source_dependent_variables=None): +def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, + source_dependent_variables=None): replacements = {} if base_kernel is not None: @@ -299,7 +301,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, source_dependent_v result_int_gs.append(0) if source_dependent_variables is not None: - result_int_gs = _reduce_number_of_fmms(result_int_gs, source_dependent_variables) + result_int_gs = _reduce_number_of_fmms(result_int_gs, + source_dependent_variables) result = [coeff + int_g for coeff, int_g in zip(result_coeffs, result_int_gs)] return np.array(result, dtype=object) @@ -359,7 +362,8 @@ def _convert_to_matrix(module, *gens): for syzygy in module: row = [] for dmp in syzygy.data: - row.append(sympy.Poly(dmp.to_dict(), *gens, domain=sympy.EX).as_expr()) + row.append(sympy.Poly(dmp.to_dict(), *gens, + domain=sympy.EX).as_expr()) result.append(row) return sympy.Matrix(result) @@ -378,7 +382,7 @@ def _convert_to_matrix(module, *gens): def _factor_left(mat, axis_vars): - return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T + return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T def _factor_right(mat, factor_left): @@ -413,7 +417,7 @@ def _check_int_gs_common(int_gs): def _reduce_number_of_fmms(int_gs, source_dependent_variables): - from pymbolic.interop.sympy import SympyToPymbolicMapper, PymbolicToSympyMapper + from pymbolic.interop.sympy import PymbolicToSympyMapper import sympy source_exprs = [] @@ -422,7 +426,6 @@ def _reduce_number_of_fmms(int_gs, source_dependent_variables): dim = int_gs[0].target_kernel.dim axis_vars = sympy.symbols(f"_x0:{dim}") to_sympy = PymbolicToSympyMapper() - to_pymbolic = SympyToPymbolicMapper() _check_int_gs_common(int_gs) @@ -447,26 +450,25 @@ def _reduce_number_of_fmms(int_gs, source_dependent_variables): mat = sympy.Matrix(matrix) lhs_mat = _factor_left(mat, axis_vars) rhs_mat = _factor_right(mat, lhs_mat) - + if rhs_mat.shape[0] >= mat.shape[0]: return int_gs rhs_mat = rhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) lhs_mat = lhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) - rhs_polys = [0] * rhs_mat.shape[0] base_kernel = int_gs[0].source_kernels[0].get_base_kernel() base_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) - rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) \ + rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) for poly in row] for row in rhs_mat.tolist()] - + rhs_int_gs = [] for i in range(rhs_mat.shape[0]): source_kernels = [] densities = [] for j in range(rhs_mat.shape[1]): - new_densities = [density * source_expr[j] for density in \ + new_densities = [density * source_exprs[j] for density in rhs_mat_int_gs[i][j].densities] source_kernels.extend(rhs_mat_int_gs[i][j].source_kernels) densities.extend(new_densities) @@ -495,7 +497,7 @@ def __call__(self, expr): return {1: expr} rec = __call__ - + def map_sum(self, expr): d = defaultdict(lambda: 0) for child in expr.children: @@ -503,7 +505,7 @@ def map_sum(self, expr): for k, v in d_child.items(): d[k] += v return dict(d) - + def map_product(self, expr): if len(expr.children) > 2: left = Product(tuple(expr.children[:2])) @@ -517,7 +519,7 @@ def map_product(self, expr): left, right = expr.children d_left = self.rec(left) d_right = self.rec(right) - d = defaultdict(lambda : 0) + d = defaultdict(lambda: 0) for k_left, v_left in d_left.items(): for k_right, v_right in d_right.items(): d[k_left*k_right] += v_left*v_right @@ -696,8 +698,9 @@ def _merge_int_g_expr(expr, replacements): int_g.kernel_arguments) source_kernels = result_int_g.source_kernels + int_g.source_kernels densities = result_int_g.densities + int_g.densities - new_source_kernels, new_densities = \ - _merge_source_kernel_duplicates(source_kernels, densities) + new_source_kernels, new_densities = source_kernels, densities + # new_source_kernels, new_densities = \ + # _merge_source_kernel_duplicates(source_kernels, densities) result_int_g = result_int_g.copy( source_kernels=tuple(new_source_kernels), densities=tuple(new_densities), @@ -744,12 +747,13 @@ def _merge_int_g_expr(expr, replacements): kernels = [StokesletKernel(3, 0, 2), StokesletKernel(3, 0, 0)] kernels += [StressletKernel(3, 0, 0, 0), StressletKernel(3, 0, 0, 1), StressletKernel(3, 0, 0, 2), StressletKernel(3, 0, 1, 2)] - + sym_d = make_sym_vector("d", base_kernel.dim) sym_r = sqrt(sum(a**2 for a in sym_d)) conv = SympyToPymbolicMapper() expression_knl = ExpressionKernel(3, conv(sym_d[0]*sym_d[1]/sym_r**3), 1, False) - expression_knl2 = ExpressionKernel(3, conv(1/sym_r + sym_d[0]*sym_d[0]/sym_r**3), 1, False) + expression_knl2 = ExpressionKernel(3, conv(1/sym_r + sym_d[0]*sym_d[0]/sym_r**3), + 1, False) kernels = [expression_knl, expression_knl2] #kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), # ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] From 3f3817585173a1c17fe4f953e67d3c1070e5a4d0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 21 Jun 2021 14:25:51 -0500 Subject: [PATCH 069/209] pass translation_classes_data --- pytential/qbx/__init__.py | 4 ++++ pytential/qbx/fmm.py | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pytential/qbx/__init__.py b/pytential/qbx/__init__.py index 3c5845c44..b8927cfdf 100644 --- a/pytential/qbx/__init__.py +++ b/pytential/qbx/__init__.py @@ -632,6 +632,9 @@ def exec_compute_potential_insn_fmm(self, actx: PyOpenCLArrayContext, geo_data.tree().user_source_ids, insn.kernel_arguments, evaluate)) + from sumpy.fmm import SumpyTranslationClassesData + translation_classes_data = SumpyTranslationClassesData(actx.queue, geo_data.traversal()) + wrangler = self.expansion_wrangler_code_container( target_kernels=insn.target_kernels, source_kernels=insn.source_kernels).get_wrangler( @@ -640,6 +643,7 @@ def exec_compute_potential_insn_fmm(self, actx: PyOpenCLArrayContext, self.fmm_level_to_order, source_extra_kwargs=source_extra_kwargs, kernel_extra_kwargs=kernel_extra_kwargs, + translation_classes_data=translation_classes_data, _use_target_specific_qbx=self._use_target_specific_qbx) from pytential.qbx.geometry import target_state diff --git a/pytential/qbx/fmm.py b/pytential/qbx/fmm.py index 787c44e56..ef014f4cb 100644 --- a/pytential/qbx/fmm.py +++ b/pytential/qbx/fmm.py @@ -89,6 +89,7 @@ def get_wrangler(self, queue, geo_data, dtype, qbx_order, fmm_level_to_order, source_extra_kwargs=None, kernel_extra_kwargs=None, + translation_classes_data=None, *, _use_target_specific_qbx=False): if source_extra_kwargs is None: @@ -99,7 +100,8 @@ def get_wrangler(self, queue, geo_data, dtype, qbx_order, fmm_level_to_order, source_extra_kwargs, kernel_extra_kwargs, - _use_target_specific_qbx) + _use_target_specific_qbx=_use_target_specific_qbx, + translation_classes_data=translation_classes_data) class QBXExpansionWrangler(SumpyExpansionWrangler): @@ -124,13 +126,11 @@ class QBXExpansionWrangler(SumpyExpansionWrangler): def __init__(self, code_container, queue, geo_data, dtype, qbx_order, fmm_level_to_order, source_extra_kwargs, kernel_extra_kwargs, - _use_target_specific_qbx=None): - if _use_target_specific_qbx: - raise ValueError("TSQBX is not implemented in sumpy") - + translation_classes_data=None, *, _use_target_specific_qbx=None): SumpyExpansionWrangler.__init__(self, code_container, queue, geo_data.tree(), - dtype, fmm_level_to_order, source_extra_kwargs, kernel_extra_kwargs) + dtype, fmm_level_to_order, source_extra_kwargs, kernel_extra_kwargs, + translation_classes_data=translation_classes_data) self.qbx_order = qbx_order self.geo_data = geo_data From e343a6eed6d88b15b1d2bd490f11dab9a59bb40e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 21 Jun 2021 14:59:27 -0500 Subject: [PATCH 070/209] Merge equal source_kernels in IntG --- pytential/symbolic/primitives.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 8cb071b26..505dc79f9 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -23,6 +23,7 @@ from sys import intern from warnings import warn from functools import wraps, partial +from collections import OrderedDict import numpy as np from pymbolic.primitives import ( # noqa: F401,N813 @@ -1502,10 +1503,16 @@ def __init__(self, target_kernel, source_kernels, densities, raise ValueError("invalid value (%s) of qbx_forced_limit" % qbx_forced_limit) - densities = list(densities) - source_kernels = tuple([source_kernels[i] for i in range(len(densities)) - if densities[i] != 0]) - densities = tuple([density for density in densities if density != 0]) + knl_density_dict = OrderedDict() + for density, source_kernel in zip(densities, source_kernels): + if source_kernel in knl_density_dict: + knl_density_dict[source_kernel] += density + else: + knl_density_dict[source_kernel] = density + + knl_density_dict = OrderedDict((k, v) for k, v in knl_density_dict.items() if v) + densities = tuple(knl_density_dict.values()) + source_kernels = tuple(knl_density_dict.keys()) kernel_arg_names = set() From 3cc6d8fb184161cd74051c3757f7f7c3bd14228c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 4 Jul 2021 18:55:07 -0500 Subject: [PATCH 071/209] remove half space elasticity for now --- pytential/symbolic/elasticity.py | 245 +----------------------------- test/test_elasticity2.py | 250 ------------------------------- 2 files changed, 3 insertions(+), 492 deletions(-) delete mode 100644 test/test_elasticity2.py diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 9288f95c1..1db4543b1 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -23,249 +23,10 @@ import numpy as np from pytential import sym -from pytential.symbolic.pde.system_utils import merge_int_g_exprs -from sumpy.kernel import (LineOfCompressionKernel, - AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier, - LaplaceKernel) +from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, + TargetPointMultiplier, LaplaceKernel) from pymbolic import var -from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase, - StokesOperator) -from pytential.symbolic.primitives import NodeCoordinateComponent - - -class KelvinOperator(StokesOperator): - """Representation for free space Green's function for elasticity commonly - known as the Kelvin solution [1] given by Lord Kelvin. - - [1] Gimbutas, Z., & Greengard, L. (2016). A fast multipole method for the - evaluation of elastostatic fields in a half-space with zero normal stress. - Advances in Computational Mathematics, 42(1), 175-198. - - .. automethod:: __init__ - .. automethod:: operator - """ - - def __init__(self, method="laplace", mu_sym=var("mu"), nu_sym=0.5): - if nu_sym == 0.5: - raise ValueError("poisson's ratio cannot be 0.5") - - if method not in ["laplace", "biharmonic"]: - raise ValueError(f"invalid method: {method}. " - "Needs to be one of laplace or biharmonic") - super().__init__(ambient_dim=3, side=+1, method=method, - mu_sym=mu_sym, nu_sym=nu_sym) - self.laplace_kernel = LaplaceKernel(3) - - def operator(self, sigma, *, normal, qbx_forced_limit="avg"): - return self.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit) - - def get_density_var(self, name="sigma"): - """ - :returns: a symbolic vector corresponding to the density. - """ - return sym.make_sym_vector(name, 3) - - -class MindlinOperator: - """Representation for elasticity in a half-space with zero normal stress which - is based on Mindlin's explicit solution. See [1] and [2]. - - [1] Mindlin, R. D. (1936). Force at a point in the interior of a semi‐infinite - solid. Physics, 7(5), 195-202. - - [2] Gimbutas, Z., & Greengard, L. (2016). A fast multipole method for the - evaluation of elastostatic fields in a half-space with zero normal stress. - Advances in Computational Mathematics, 42(1), 175-198. - - .. automethod:: __init__ - .. automethod:: operator - .. automethod:: free_space_operator - .. automethod:: get_density_var - """ - - def __init__(self, *, method="biharmonic", mu_sym=var("mu"), nu_sym=var("nu"), - line_of_compression_tol=0): - if method not in ["biharmonic", "laplace"]: - raise ValueError(f"invalid method: {method}." - "Needs to be one of laplace, biharmonic") - - self.method = method - self.mu = mu_sym - self.nu = nu_sym - self.free_space_op = KelvinOperator(method=method, mu_sym=mu_sym, - nu_sym=nu_sym) - self.modified_free_space_op = KelvinOperator( - method=method, mu_sym=mu_sym + 4*nu_sym, nu_sym=-nu_sym) - self.compression_knl = LineOfCompressionKernel(3, 2, mu_sym, nu_sym, - tol=line_of_compression_tol) - self.ambient_dim = 3 - - def K(self, sigma, normal, qbx_forced_limit): - return merge_int_g_exprs(self.free_space_op.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit)) - - def A(self, sigma, normal, qbx_forced_limit): - result = -self.modified_free_space_op.stresslet.apply(sigma, normal, - qbx_forced_limit=qbx_forced_limit) - - new_density = sum(a*b for a, b in zip(sigma, normal)) - int_g = sym.S(self.free_space_op.laplace_kernel, new_density, - qbx_forced_limit=qbx_forced_limit) - - for i in range(3): - temp = 2*int_g.copy( - target_kernel=AxisTargetDerivative(i, int_g.target_kernel)) - if i == 2: - temp *= -1 - result[i] += temp - return result - - def _create_int_g(self, knl, source_deriv_dirs, target_deriv_dirs, density, - **kwargs): - - for deriv_dir in target_deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - if deriv_dir == 2: - density *= -1 - - args = [arg.loopy_arg.name for arg in knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) - - res = sym.S(knl, density, **kwargs) - return res - - def B(self, sigma, normal, qbx_forced_limit): - sym_expr = np.zeros((3,), dtype=object) - mu = self.mu - nu = self.nu - lam = 2*nu*mu/(1-2*nu) - sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) - - source_kernel_dirs = [[0, 0], [1, 1], [2, 2], [0, 1]] - densities = [ - sigma[0]*normal[0]*2*mu, - sigma[1]*normal[1]*2*mu, - -sigma[2]*normal[2]*2*mu - 2*lam*sigma_normal_product, - (sigma[0]*normal[1] + sigma[1]*normal[0])*2*mu, - ] - source_kernels = [ - AxisSourceDerivative(a, AxisSourceDerivative(b, self.compression_knl)) - for a, b in source_kernel_dirs - ] - - kwargs = {"qbx_forced_limit": qbx_forced_limit} - args = [arg.loopy_arg.name for arg in self.compression_knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) - - int_g = sym.IntG(source_kernels=tuple(source_kernels), - target_kernel=self.compression_knl, densities=tuple(densities), - **kwargs) - - for i in range(3): - sym_expr[i] = int_g.copy(target_kernel=AxisTargetDerivative( - i, int_g.target_kernel)) - - return sym_expr - - def C(self, sigma, normal, qbx_forced_limit): - result = np.zeros((3,), dtype=object) - mu = self.mu - nu = self.nu - lam = 2*nu*mu/(1-2*nu) - alpha = (lam + mu)/(lam + 2*mu) - y = [NodeCoordinateComponent(i) for i in range(3)] - sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) - - laplace_kernel = self.free_space_op.laplace_kernel - densities = [] - source_kernels = [] - - # phi_c in Gimbutas et, al. - densities.extend([ - -2*alpha*mu*y[2]*sigma[0]*normal[0], - -2*alpha*mu*y[2]*sigma[1]*normal[1], - -2*alpha*mu*y[2]*sigma[2]*normal[2], - -2*alpha*mu*y[2]*(sigma[0]*normal[1] + sigma[1]*normal[0]), - +2*alpha*mu*y[2]*(sigma[0]*normal[2] + sigma[2]*normal[0]), - +2*alpha*mu*y[2]*(sigma[1]*normal[2] + sigma[2]*normal[1]), - ]) - source_kernel_dirs = [[0, 0], [1, 1], [2, 2], [0, 1], [0, 2], [1, 2]] - source_kernels.extend([ - AxisSourceDerivative(a, AxisSourceDerivative(b, laplace_kernel)) - for a, b in source_kernel_dirs - ]) - - # G in Gimbutas et, al. - densities.extend([ - (2*alpha - 2)*y[2]*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), - (2*alpha - 2)*y[2]*mu*(sigma[1]*normal[2] + sigma[2]*normal[1]), - (2*alpha - 2)*y[2]*(mu*-2*sigma[2]*normal[2] - - lam*sigma_normal_product), - ]) - source_kernels.extend( - [AxisSourceDerivative(i, laplace_kernel) for i in range(3)]) - - int_g = sym.IntG(source_kernels=tuple(source_kernels), - target_kernel=laplace_kernel, densities=tuple(densities), - qbx_forced_limit=qbx_forced_limit) - - for i in range(3): - result[i] = int_g.copy(target_kernel=AxisTargetDerivative( - i, int_g.target_kernel)) - - if i == 2: - # Target derivative w.r.t x[2] is flipped due to target image - result[i] *= -1 - - # H in Gimubtas et, al. - densities = [ - (-2)*(2 - alpha)*mu*sigma[0]*normal[0], - (-2)*(2 - alpha)*mu*sigma[1]*normal[1], - (-2)*(2 - alpha)*mu*sigma[2]*normal[2], - (-2)*(2 - alpha)*mu*(sigma[0]*normal[1] + sigma[1]*normal[0]), - (+2)*(2 - alpha)*mu*(sigma[0]*normal[2] + sigma[2]*normal[0]), - (+2)*(2 - alpha)*mu*(sigma[1]*normal[2] + sigma[2]*normal[1]), - ] - source_kernel_dirs = [[0, 0], [1, 1], [2, 2], [0, 1], [0, 2], [1, 2]] - source_kernels = [ - AxisSourceDerivative(a, AxisSourceDerivative(b, laplace_kernel)) - for a, b in source_kernel_dirs - ] - H = sym.IntG(source_kernels=tuple(source_kernels), - target_kernel=laplace_kernel, densities=tuple(densities), - qbx_forced_limit=qbx_forced_limit) - result[2] -= H - - return result - - def free_space_operator(self, sigma, *, normal, qbx_forced_limit="avg"): - return self.free_space_op.operator(sigma=sigma, normal=normal, - qbx_forced_limit=qbx_forced_limit) - - def operator(self, sigma, *, normal, qbx_forced_limit="avg"): - resultA = self.A(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - resultC = self.C(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - resultB = self.B(sigma, normal=normal, qbx_forced_limit=qbx_forced_limit) - - if self.method == "biharmonic": - # A and C are both derivatives of Biharmonic Green's function - # TODO: make merge_int_g_exprs smart enough to merge two different - # kernels into two separate IntGs. - result = merge_int_g_exprs(resultA + resultC, - base_kernel=self.free_space_op.stresslet.base_kernel) - result += merge_int_g_exprs(resultB, base_kernel=self.compression_knl) - return result - else: - return resultA + resultB + resultC - - def get_density_var(self, name="sigma"): - """ - :returns: a symbolic vector corresponding to the density. - """ - return sym.make_sym_vector(name, 3) +from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase) class StressletWrapperYoshida(StressletWrapperBase): diff --git a/test/test_elasticity2.py b/test/test_elasticity2.py deleted file mode 100644 index f17b99fa1..000000000 --- a/test/test_elasticity2.py +++ /dev/null @@ -1,250 +0,0 @@ -__copyright__ = "Copyright (C) 2017 Natalie Beams" - -__license__ = """ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -import numpy as np -import pyopencl as cl - -from arraycontext import PyOpenCLArrayContext -from meshmode.discretization import Discretization -from meshmode.discretization.poly_element import \ - InterpolatoryQuadratureSimplexGroupFactory - -from pytools.obj_array import make_obj_array - -from pytential import bind, sym -from pytential import GeometryCollection - -import pytest -from pyopencl.tools import ( # noqa - pytest_generate_tests_for_pyopencl - as pytest_generate_tests) - -import logging -logger = logging.getLogger(__name__) - - -# {{{ test_exterior_stokes - -def run_exterior_stokes(actx, *, - ambient_dim, target_order, qbx_order, resolution, - fmm_order=None, # FIXME: FMM is slower than direct evaluation - source_ovsmp=None, - radius=1.5, - mu=1.0, - nu=0.4, - verbose=False, - method="naive", - - _target_association_tolerance=0.05, - _expansions_in_tree_have_extent=True): - # {{{ geometry - - if source_ovsmp is None: - source_ovsmp = 4 if ambient_dim == 2 else 8 - - places = {} - - if ambient_dim == 3: - from meshmode.mesh.generation import generate_surface_of_revolution - nheight = 10*(resolution + 1) - nangle = 10*(resolution + 1) - mesh = generate_surface_of_revolution( - lambda x, y: np.ones(x.shape), - np.linspace(-1.5, -0.5, nheight), - np.linspace(0, 2*np.pi, nangle, endpoint=False), target_order) - else: - raise ValueError(f"unsupported dimension: {ambient_dim}") - - mesh_vertices_image = mesh.vertices.copy() - mesh_vertices_image[2, :] *= -1 - mesh_grp, = mesh.groups - mesh_grp_nodes_image = mesh_grp.nodes.copy() - mesh_grp_nodes_image[2, :] *= -1 - mesh_grp_image = mesh_grp.copy(nodes=mesh_grp_nodes_image) - mesh_image = mesh.copy(vertices=mesh_vertices_image, groups=[mesh_grp_image]) - - pre_density_discr = Discretization(actx, mesh, - InterpolatoryQuadratureSimplexGroupFactory(target_order)) - pre_density_discr_image = pre_density_discr.copy(mesh=mesh_image) - - from pytential.qbx import QBXLayerPotentialSource - qbx = QBXLayerPotentialSource(pre_density_discr, - fine_order=source_ovsmp * target_order, - qbx_order=qbx_order, - fmm_order=fmm_order, - _max_leaf_refine_weight=64, - target_association_tolerance=_target_association_tolerance, - _expansions_in_tree_have_extent=_expansions_in_tree_have_extent) - places["source"] = qbx - places["target"] = qbx - places["target_image"] = qbx.copy(density_discr=pre_density_discr_image) - - from extra_int_eq_data import make_source_and_target_points - point_source, point_target = make_source_and_target_points( - actx, - side=+1, - inner_radius=0.5 * radius, - outer_radius=2.0 * radius, - ambient_dim=ambient_dim, - ) - places["point_source"] = point_source - places["point_target"] = point_target - - places = GeometryCollection(places, auto_where="source") - - density_discr = places.get_discretization("source") - logger.info("ndofs: %d", density_discr.ndofs) - logger.info("nelements: %d", density_discr.mesh.nelements) - - # }}} - - # {{{ symbolic - - sym_normal = sym.make_sym_vector("normal", ambient_dim) - sym_mu = sym.var("mu") - sym_nu = sym.var("nu") - - if ambient_dim == 3: - from pytential.symbolic.elasticity import MindlinOperator - op = MindlinOperator(method=method, mu_sym=sym_mu, nu_sym=sym_nu) - else: - raise AssertionError() - - sym_sigma = op.get_density_var("sigma") - sym_bc = op.get_density_var("bc") - sym_rhs = sym_bc - sym_source_pot = op.free_space_op.stokeslet.apply(sym_sigma, qbx_forced_limit=None) - - # }}} - - # {{{ boundary conditions - - normal = bind(places, sym.normal(ambient_dim).as_vector())(actx) - - np.random.seed(42) - charges = make_obj_array([ - actx.from_numpy(np.random.randn(point_source.ndofs)) - for _ in range(ambient_dim) - ]) - - bc_context = {"nu": nu} - op_context = {"mu": mu, "normal": normal, "nu": nu} - direct_context = {"mu": mu, "nu": nu} - - bc_op = bind(places, sym_source_pot, - auto_where=("point_source", "source")) - bc = bc_op(actx, sigma=charges, **direct_context) - - rhs = bind(places, sym_rhs)(actx, bc=bc, **bc_context) - - sym_op = op.free_space_operator(sym_sigma, normal=sym_normal, qbx_forced_limit=1) - sym_op_image = op.operator(sym_sigma, normal=sym_normal, qbx_forced_limit=2) - bound_op = bind(places, sym_op, auto_where=("source", "target")) - bound_op_image = bind(places, sym_op_image, auto_where=("source", "target_image")) - # }}} - - def print_timing_data(timings, name): - result = {k: 0 for k in list(timings.values())[0].keys()} - total = 0 - for k, timing in timings.items(): - for k, v in timing.items(): - result[k] += v['wall_elapsed'] - total += v['wall_elapsed'] - result['total'] = total - print(f"{name}={result}") - - fmm_timing_data = {} - v1 = bound_op.eval({"sigma": rhs, **op_context}, array_context=actx, - timing_data=fmm_timing_data) - print_timing_data(fmm_timing_data, method) - - fmm_timing_data = {} - v2 = bound_op_image.eval({"sigma": rhs, **op_context}, array_context=actx, - timing_data=fmm_timing_data) - print_timing_data(fmm_timing_data, method) - - h_max = bind(places, sym.h_max(ambient_dim))(actx) - - return h_max, v1 + v2 - - -@pytest.mark.parametrize("method, nu", [ - ("biharmonic", 0.4), - ("laplace", 0.4), - ]) -def test_exterior_stokes(ctx_factory, method, nu, verbose=False): - if verbose: - logging.basicConfig(level=logging.INFO) - - def rnorm2(x, y): - y_norm = actx.np.linalg.norm(y.dot(y), ord=2) - if y_norm < 1.0e-14: - y_norm = 1.0 - - d = x - y - return actx.np.linalg.norm(d.dot(d), ord=2) / y_norm - - target_order = 3 - qbx_order = 3 - cl_ctx = cl.create_some_context() - queue = cl.CommandQueue(cl_ctx, - properties=cl.command_queue_properties.PROFILING_ENABLE) - actx = PyOpenCLArrayContext(queue) - fmm_order = 6 - resolution = 0 - ambient_dim = 3 - - kwargs = dict( - actx=actx, - ambient_dim=ambient_dim, - target_order=target_order, - qbx_order=qbx_order, - resolution=resolution, - verbose=verbose, - nu=nu, - method=method, - ) - - h_max, fmm_result = run_exterior_stokes(fmm_order=fmm_order, **kwargs) - _, direct_result = run_exterior_stokes(fmm_order=False, **kwargs) - - v_error = rnorm2(fmm_result, direct_result) - print(v_error) - - assert v_error < 1e-5 - - -# }}} - - -# You can test individual routines by typing -# $ python test_stokes.py 'test_routine()' - -if __name__ == "__main__": - import sys - if len(sys.argv) > 1: - exec(sys.argv[1]) - else: - from pytest import main - main([__file__]) - -# vim: fdm=marker From e095e6cd496c53bff0d5f74595784d70076226e5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 4 Jul 2021 18:59:02 -0500 Subject: [PATCH 072/209] Fix formatting --- pytential/qbx/__init__.py | 3 ++- pytential/symbolic/elasticity.py | 6 +++--- pytential/symbolic/execution.py | 2 +- pytential/symbolic/pde/system_utils.py | 4 ++-- pytential/symbolic/primitives.py | 3 ++- pytential/symbolic/stokes.py | 22 +++++++++++----------- test/test_stokes.py | 7 ++++--- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pytential/qbx/__init__.py b/pytential/qbx/__init__.py index 6a63d5eb4..c1ba6bb2d 100644 --- a/pytential/qbx/__init__.py +++ b/pytential/qbx/__init__.py @@ -635,7 +635,8 @@ def exec_compute_potential_insn_fmm(self, actx: PyOpenCLArrayContext, insn.kernel_arguments, evaluate)) from sumpy.fmm import SumpyTranslationClassesData - translation_classes_data = SumpyTranslationClassesData(actx.queue, geo_data.traversal()) + translation_classes_data = SumpyTranslationClassesData(actx.queue, + geo_data.traversal()) wrangler = self.expansion_wrangler_code_container( target_kernels=insn.target_kernels, diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 1db4543b1..92c52f85c 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -49,7 +49,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.nu = nu_sym def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): return self.apply_stokeslet_and_stresslet([0]*self.dim, density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1, extra_deriv_dirs) @@ -57,7 +57,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): mu = self.mu nu = self.nu @@ -159,7 +159,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): stresslet = StressletWrapperYoshida(3, self.mu, self.nu) return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, diff --git a/pytential/symbolic/execution.py b/pytential/symbolic/execution.py index fe617ecf6..6771fede9 100644 --- a/pytential/symbolic/execution.py +++ b/pytential/symbolic/execution.py @@ -1142,7 +1142,7 @@ def __call__(self, *args, **kwargs): raise TypeError("More than one positional argument supplied. " "None or an PyOpenCLArrayContext expected.") - timing_data = kwargs.pop('timing_data', None) + timing_data = kwargs.pop("timing_data", None) return self.eval(kwargs, array_context=array_context, timing_data=timing_data) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 04334ae6a..6585a35b8 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -740,8 +740,8 @@ def _merge_int_g_expr(expr, replacements): if __name__ == "__main__": - from sumpy.kernel import (StokesletKernel, BiharmonicKernel, StressletKernel, - ElasticityKernel, LaplaceKernel) + from sumpy.kernel import (StokesletKernel, BiharmonicKernel, # noqa:F401 + StressletKernel, ElasticityKernel, LaplaceKernel) base_kernel = BiharmonicKernel(3) #base_kernel = LaplaceKernel(3) kernels = [StokesletKernel(3, 0, 2), StokesletKernel(3, 0, 0)] diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index f1a38b3ae..1fad70a74 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1506,7 +1506,8 @@ def __init__(self, target_kernel, source_kernels, densities, else: knl_density_dict[source_kernel] = density - knl_density_dict = OrderedDict((k, v) for k, v in knl_density_dict.items() if v) + knl_density_dict = OrderedDict((k, v) for k, v in + knl_density_dict.items() if v) densities = tuple(knl_density_dict.values()) source_kernels = tuple(knl_density_dict.keys()) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index be06f53ea..8b1618668 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -73,7 +73,7 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """Symbolic expressions for integrating Stokeslet kernel. Returns an object array of symbolic expressions for the vector @@ -171,7 +171,7 @@ def __init__(self, dim, mu_sym, nu_sym): self.nu = nu_sym def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): """Symbolic expressions for integrating Stresslet kernel. Returns an object array of symbolic expressions for the vector @@ -278,7 +278,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) return res - def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): sym_expr = np.zeros((self.dim,), dtype=object) @@ -349,7 +349,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5, method="biharmonic"): self.kernel_dict[(i, j, k)] = self.kernel_dict[s] # For elasticity (nu != 0.5), we need the LaplaceKernel - self.kernel_dict['laplace'] = LaplaceKernel(self.dim) + self.kernel_dict["laplace"] = LaplaceKernel(self.dim) def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): @@ -364,7 +364,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, coeffs = [1] extra_deriv_dirs_vec = [[]] - kernel_indices = [idx, 'laplace', 'laplace', 'laplace'] + kernel_indices = [idx, "laplace", "laplace", "laplace"] dir_vec_indices = [idx[-1], idx[1], idx[0], idx[2]] coeffs = [1, (1 - 2*nu)/self.dim, -(1 - 2*nu)/self.dim, -(1 - 2*nu)] extra_deriv_dirs_vec = [[], [idx[0]], [idx[1]], [idx[2]]] @@ -382,7 +382,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, return result/(2*(1 - nu)) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): sym_expr = np.zeros((self.dim,), dtype=object) @@ -398,7 +398,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): stokeslet_obj = StokesletWrapper(dim=self.dim, mu_sym=self.mu, nu_sym=self.nu, method=self.method) @@ -466,14 +466,14 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.nu = nu_sym def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): return self.apply_stokeslet_and_stresslet([0]*self.dim, density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1, extra_deriv_dirs) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, - extra_deriv_dirs=[]): + extra_deriv_dirs=()): sym_expr = np.zeros((self.dim,), dtype=object) @@ -549,7 +549,7 @@ def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): self.mu = mu_sym self.nu = nu_sym - def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=[]): + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): stresslet = StressletWrapperTornberg(3, self.mu, self.nu) return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, @@ -775,7 +775,7 @@ def _operator(self, sigma, normal, qbx_forced_limit): return self.stresslet.apply_stokeslet_and_stresslet(sigma, sigma, normal, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=self.eta, stresslet_weight=1, - extra_deriv_dirs=[]) + extra_deriv_dirs=()) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 diff --git a/test/test_stokes.py b/test/test_stokes.py index 3445449da..ebae7b233 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -216,9 +216,9 @@ def print_timing_data(timings, name): total = 0 for k, timing in timings.items(): for k, v in timing.items(): - result[k] += v['wall_elapsed'] - total += v['wall_elapsed'] - result['total'] = total + result[k] += v["wall_elapsed"] + total += v["wall_elapsed"] + result["total"] = total print(f"{name}={result}") print_timing_data(fmm_timing_data, method) @@ -285,6 +285,7 @@ def rnorm2(x, y): return h_max, v_error + @pytest.mark.parametrize("ambient_dim, method, nu", [ (2, "naive", 0.5), (2, "biharmonic", 0.5), From b7fb387a6a2e3d2d7338657530b72a7691463192 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 23 Aug 2021 02:27:11 -0500 Subject: [PATCH 073/209] Fix translation_classes_data for fmmlib --- pytential/qbx/fmmlib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/qbx/fmmlib.py b/pytential/qbx/fmmlib.py index d24a50990..3c2f69eb8 100644 --- a/pytential/qbx/fmmlib.py +++ b/pytential/qbx/fmmlib.py @@ -56,7 +56,8 @@ def __init__(self, cl_context, def get_wrangler(self, queue, geo_data, dtype, qbx_order, fmm_level_to_order, source_extra_kwargs=None, - kernel_extra_kwargs=None, + kernel_extra_kwargs=None, *, + translation_classes_data=None, _use_target_specific_qbx=None): if source_extra_kwargs is None: From e44bee7164b9a5ecb5fbc780bf588b881d312ced Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 23 Aug 2021 02:31:29 -0500 Subject: [PATCH 074/209] Fix B008 Do not perform function calls in argument defaults --- pytential/symbolic/elasticity.py | 8 ++++---- pytential/symbolic/stokes.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 92c52f85c..64e6e766b 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -25,8 +25,8 @@ from pytential import sym from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier, LaplaceKernel) -from pymbolic import var -from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase) +from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase, + _MU_SYM_DEFAULT) class StressletWrapperYoshida(StressletWrapperBase): @@ -39,7 +39,7 @@ class StressletWrapperYoshida(StressletWrapperBase): International Journal for Numerical Methods in Engineering, 50(3), 525-547. """ - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " @@ -150,7 +150,7 @@ class StokesletWrapperYoshida(StokesletWrapperBase): International Journal for Numerical Methods in Engineering, 50(3), 525-547. """ - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 8b1618668..16283f5bc 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -41,6 +41,9 @@ # {{{ StokesletWrapper +_MU_SYM_DEFAULT = var("mu") + + class StokesletWrapperBase: """Wrapper class for the :class:`~sumpy.kernel.StokesletKernel` kernel. @@ -240,7 +243,8 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): class StokesletWrapper(StokesletWrapperBase): - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5, method="biharmonic"): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, + method="biharmonic"): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -316,7 +320,8 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): # {{{ StressletWrapper class StressletWrapper(StressletWrapperBase): - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5, method="biharmonic"): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, + method="biharmonic"): super().__init__(dim, mu_sym, nu_sym) if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to StokesletWrapper") @@ -454,7 +459,7 @@ class StressletWrapperTornberg(StressletWrapperBase): three-dimensional Stokes equations. Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " @@ -538,7 +543,7 @@ class StokesletWrapperTornberg(StokesletWrapperBase): Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim=None, mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " @@ -666,7 +671,7 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): """ def __init__(self, *, omega, alpha=None, eta=None, method="biharmonic", - mu_sym=var("mu"), nu_sym=0.5): + mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): r""" :arg omega: farfield behaviour of the velocity field, as defined by :math:`A` in [HsiaoKress1985]_ Equation 2.3. @@ -754,7 +759,8 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None, method="laplace", mu_sym=var("mu"), nu_sym=0.5): + def __init__(self, *, eta=None, method="laplace", mu_sym=_MU_SYM_DEFAULT, + nu_sym=0.5): r""" :arg eta: a parameter :math:`\eta > 0`. Choosing this parameter well can have a non-trivial effect on the conditioning of the operator. From c0d8ce811577b7d368841df63a884e051f01bfc1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 25 Aug 2021 16:29:57 -0500 Subject: [PATCH 075/209] Reduce diff and convert list to tuple --- pytential/qbx/__init__.py | 10 +++++++++- pytential/qbx/fmm.py | 9 ++++++--- pytential/symbolic/stokes.py | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pytential/qbx/__init__.py b/pytential/qbx/__init__.py index c1ba6bb2d..8e2576987 100644 --- a/pytential/qbx/__init__.py +++ b/pytential/qbx/__init__.py @@ -696,7 +696,15 @@ def exec_compute_potential_insn_fmm(self, actx: PyOpenCLArrayContext, @memoize_method def get_expansion_for_qbx_direct_eval(self, base_kernel, target_kernels): from sumpy.expansion.local import LineTaylorLocalExpansion - return LineTaylorLocalExpansion(base_kernel, self.qbx_order) + from sumpy.kernel import TargetDerivativeRemover + + # line Taylor cannot support target derivatives + txr = TargetDerivativeRemover() + if any(knl != txr(knl) for knl in target_kernels): + return self.expansion_factory.get_local_expansion_class( + base_kernel)(base_kernel, self.qbx_order) + else: + return LineTaylorLocalExpansion(base_kernel, self.qbx_order) @memoize_method def get_lpot_applier(self, target_kernels, source_kernels): diff --git a/pytential/qbx/fmm.py b/pytential/qbx/fmm.py index ef014f4cb..a957e6d44 100644 --- a/pytential/qbx/fmm.py +++ b/pytential/qbx/fmm.py @@ -88,8 +88,8 @@ def qbxl2p(self, order): def get_wrangler(self, queue, geo_data, dtype, qbx_order, fmm_level_to_order, source_extra_kwargs=None, - kernel_extra_kwargs=None, - translation_classes_data=None, *, + kernel_extra_kwargs=None, *, + translation_classes_data=None, _use_target_specific_qbx=False): if source_extra_kwargs is None: @@ -126,7 +126,10 @@ class QBXExpansionWrangler(SumpyExpansionWrangler): def __init__(self, code_container, queue, geo_data, dtype, qbx_order, fmm_level_to_order, source_extra_kwargs, kernel_extra_kwargs, - translation_classes_data=None, *, _use_target_specific_qbx=None): + *, translation_classes_data=None, _use_target_specific_qbx=None): + if _use_target_specific_qbx: + raise ValueError("TSQBX is not implemented in sumpy") + SumpyExpansionWrangler.__init__(self, code_container, queue, geo_data.tree(), dtype, fmm_level_to_order, source_extra_kwargs, kernel_extra_kwargs, diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 16283f5bc..dd17961e9 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -381,7 +381,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): knl = self.kernel_dict[kernel_idx] - result += _create_int_g(knl, deriv_dirs + extra_deriv_dirs, + result += _create_int_g(knl, deriv_dirs + tuple(extra_deriv_dirs), density=density_sym*dir_vec_sym[dir_vec_idx], qbx_forced_limit=qbx_forced_limit) * coeff return result/(2*(1 - nu)) From f68613dd38cdae4ca4f902f31e09b2f3bcc15098 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 25 Aug 2021 16:31:49 -0500 Subject: [PATCH 076/209] Fix typo and fix test --- test/test_stokes.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index ebae7b233..08e2a9010 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -164,7 +164,7 @@ def run_exterior_stokes(actx_factory, *, # Use the naive method here as biharmonic requires source derivatives # of point_source from pytential.symbolic.stokes import StokesletWrapper - sym_source_pot = StokesletWrapper(ambient_dim, mu_sym=sym_nu, + sym_source_pot = StokesletWrapper(ambient_dim, mu_sym=sym_mu, nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -221,7 +221,8 @@ def print_timing_data(timings, name): result["total"] = total print(f"{name}={result}") - print_timing_data(fmm_timing_data, method) + # print_timing_data(fmm_timing_data, method) + # {{{ solve from pytential.solve import gmres @@ -344,6 +345,10 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) if __name__ == "__main__": import sys if len(sys.argv) > 1: + import pyopencl as cl + context = cl._csc() + queue = cl.CommandQueue(context) + actx_factory = lambda : PyOpenCLArrayContext(queue) exec(sys.argv[1]) else: from pytest import main From f9c6808fd29b529fa22ea13f2310ea462aa2192a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 25 Aug 2021 17:34:46 -0500 Subject: [PATCH 077/209] use pyopencl-deprecated --- test/test_stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 08e2a9010..42e6d8fc9 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -38,7 +38,7 @@ logger = logging.getLogger(__name__) pytest_generate_tests = pytest_generate_tests_for_array_contexts([ - PytestPyOpenCLArrayContextFactory, + "pyopencl-deprecated", ]) From 023a2ff2562caa87e4b1522999271d2aea92674e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 25 Aug 2021 17:35:06 -0500 Subject: [PATCH 078/209] reduce diff --- test/test_stokes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 42e6d8fc9..b411dd3cc 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -86,7 +86,6 @@ def run_exterior_stokes(actx_factory, *, fine_order=source_ovsmp * target_order, qbx_order=qbx_order, fmm_order=fmm_order, - _max_leaf_refine_weight=64, target_association_tolerance=_target_association_tolerance, _expansions_in_tree_have_extent=_expansions_in_tree_have_extent) places["source"] = qbx @@ -116,12 +115,8 @@ def run_exterior_stokes(actx_factory, *, del mask - #places[sym.DEFAULT_SOURCE] = qbx - #places[sym.DEFAULT_TARGET] = qbx.density_discr - #places = GeometryCollection(places) places = GeometryCollection(places, auto_where="source") - #density_discr = places.get_discretization(sym.DEFAULT_SOURCE) density_discr = places.get_discretization("source") logger.info("ndofs: %d", density_discr.ndofs) logger.info("nelements: %d", density_discr.mesh.nelements) From 0b967e1d9ef9f362382b4330dcee5a81022094db Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 25 Aug 2021 17:36:36 -0500 Subject: [PATCH 079/209] fix for interactive tests --- test/test_stokes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_stokes.py b/test/test_stokes.py index b411dd3cc..75994d037 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -341,6 +341,7 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) import sys if len(sys.argv) > 1: import pyopencl as cl + from arraycontext import PyOpenCLArrayContext context = cl._csc() queue = cl.CommandQueue(context) actx_factory = lambda : PyOpenCLArrayContext(queue) From ca514b4ceb0b0536f1b3e8113a0209c4dd672550 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 25 Aug 2021 18:01:14 -0500 Subject: [PATCH 080/209] Fix pylint and flake8 issues --- pytential/symbolic/pde/system_utils.py | 4 ++-- pytential/symbolic/stokes.py | 22 +++++++++++++++------- test/test_stokes.py | 5 +++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 6585a35b8..d5f1d5bad 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -22,7 +22,7 @@ import numpy as np -from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper, sqrt +from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, DirectionalSourceDerivative, ExpressionKernel, KernelWrapper, TargetPointMultiplier) @@ -749,7 +749,7 @@ def _merge_int_g_expr(expr, replacements): StressletKernel(3, 0, 0, 2), StressletKernel(3, 0, 1, 2)] sym_d = make_sym_vector("d", base_kernel.dim) - sym_r = sqrt(sum(a**2 for a in sym_d)) + sym_r = sym.sqrt(sum(a**2 for a in sym_d)) conv = SympyToPymbolicMapper() expression_knl = ExpressionKernel(3, conv(sym_d[0]*sym_d[1]/sym_r**3), 1, False) expression_knl2 = ExpressionKernel(3, conv(1/sym_r + sym_d[0]*sym_d[0]/sym_r**3), diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index dd17961e9..df5fa40fd 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -207,6 +207,20 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): return merge_int_g_exprs(sym_expr) + def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): + """Symbolic derivative of velocity from Stokeslet. + + Returns an object array of symbolic expressions for the vector + resulting from integrating the *deriv_dir* target derivative of the + dyadic Stokeslet kernel with variable *density_vec_sym*. + + :arg deriv_dir: integer denoting the axis direction for the derivative. + :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on + to :class:`~pytential.symbolic.primitives.IntG`. + """ + return self.apply(density_vec_sym, qbx_forced_limit, [deriv_dir]) + def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. @@ -670,7 +684,7 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=None, eta=None, method="biharmonic", + def __init__(self, *, omega, alpha=1.0, eta=1.0, method="biharmonic", mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): r""" :arg omega: farfield behaviour of the velocity field, as defined @@ -688,12 +702,6 @@ def __init__(self, *, omega, alpha=None, eta=None, method="biharmonic", # so we choose alpha = eta = 1, which seems to be in line with some # of the presented numerical results too. - if alpha is None: - alpha = 1.0 - - if eta is None: - eta = 1.0 - self.omega = omega self.alpha = alpha self.eta = eta diff --git a/test/test_stokes.py b/test/test_stokes.py index 75994d037..f9b69789e 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -32,7 +32,6 @@ from meshmode import _acf # noqa: F401 from arraycontext import pytest_generate_tests_for_array_contexts -from meshmode.array_context import PytestPyOpenCLArrayContextFactory import logging logger = logging.getLogger(__name__) @@ -344,7 +343,9 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) from arraycontext import PyOpenCLArrayContext context = cl._csc() queue = cl.CommandQueue(context) - actx_factory = lambda : PyOpenCLArrayContext(queue) + def actx_factory(): + return PyOpenCLArrayContext(queue) + exec(sys.argv[1]) else: from pytest import main From b85c9ec096a64a3b552cefb77d9b78bc277f9331 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 26 Aug 2021 09:30:35 -0500 Subject: [PATCH 081/209] make sure both operands are tuples before adding them --- pytential/symbolic/stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index df5fa40fd..f8545a473 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -395,7 +395,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): knl = self.kernel_dict[kernel_idx] - result += _create_int_g(knl, deriv_dirs + tuple(extra_deriv_dirs), + result += _create_int_g(knl, tuple(deriv_dirs) + tuple(extra_deriv_dirs), density=density_sym*dir_vec_sym[dir_vec_idx], qbx_forced_limit=qbx_forced_limit) * coeff return result/(2*(1 - nu)) From c8ff6e194c1565e9777ceacd49d5bc6066404301 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 26 Aug 2021 09:33:18 -0500 Subject: [PATCH 082/209] Fix apply_derivative for Stresslet --- pytential/symbolic/stokes.py | 7 +++++-- test/test_stokes.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index f8545a473..c376e4295 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -207,7 +207,8 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): return merge_int_g_exprs(sym_expr) - def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): + def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, + qbx_forced_limit): """Symbolic derivative of velocity from Stokeslet. Returns an object array of symbolic expressions for the vector @@ -216,10 +217,12 @@ def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): :arg deriv_dir: integer denoting the axis direction for the derivative. :arg density_vec_sym: a symbolic vector variable for the density vector. + :arg dir_vec_sym: a symbolic vector variable for the normal direction. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. """ - return self.apply(density_vec_sym, qbx_forced_limit, [deriv_dir]) + return self.apply(density_vec_sym, dir_vec_sym, qbx_forced_limit, + [deriv_dir]) def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, qbx_forced_limit): diff --git a/test/test_stokes.py b/test/test_stokes.py index f9b69789e..0140a9d17 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -343,6 +343,7 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) from arraycontext import PyOpenCLArrayContext context = cl._csc() queue = cl.CommandQueue(context) + def actx_factory(): return PyOpenCLArrayContext(queue) From 8877c1a4e84f6c75cfd3e0d0856634aa583c0bc7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 26 Aug 2021 10:42:21 -0500 Subject: [PATCH 083/209] document merge_int_g_exprs --- pytential/symbolic/pde/system_utils.py | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index d5f1d5bad..58de057ab 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -265,6 +265,43 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, source_dependent_variables=None): + """ + Merge expressions involving :class:`~pytential.symbolic.primitives.IntG` + objects. + + Several techniques are used for merging and reducing number of FMMs + + * When `base_kernel` is given an `IntG` is rewritten using `base_kernel` + and its derivatives. + + * :class:`sumpy.kernel.AxisTargetDerivative` instances are converted + to :class:`sumpy.kernel.AxisSourceDerivative` instances. + + * If there is a sum of two `IntG`s with same target derivative and different + source derivatives of the same kernel, they are merged into one FMM. + + * If possible, convert :class:`sumpy.kernel.AxisSourceDerivative` to + :class:`sumpy.kernel.DirectionalSourceDerivative`. + + * Reduce the number of FMMs by converting the `IntG` expression to + a matrix and factoring the matrix where the left operand matrix represents + a transformation at target and the right matrix represents a transformation + at source. For this to work, we need to know which variables depend on + source so that they do not end up in the left operand. User needs to supply + this as the argument `source_dependent_variable`. + + :arg base_kernel: A :class:`sumpy.kernel.Kernel` object if given will be used + for converting a :class:`~pytential.symbolic.primitives.IntG` to a linear + expression of same type with the kernel replaced by base_kernel and its + derivatives + + :arg verbose: increase verbosity of merging + + :arg source_dependent_variable: When merging expressions, consider only these + variables as dependent on source. Otherwise consider all variables + as source dependent. This is important when reducing the number of FMMs + needed for the output. + """ replacements = {} if base_kernel is not None: From 6374375ab5051f0537bb55109492b413003f58e9 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 26 Aug 2021 13:08:54 -0500 Subject: [PATCH 084/209] Split off reduce_fmms to a new file --- pytential/symbolic/pde/reduce_fmms.py | 292 +++++++++++++++++++++++++ pytential/symbolic/pde/system_utils.py | 204 +---------------- 2 files changed, 295 insertions(+), 201 deletions(-) create mode 100644 pytential/symbolic/pde/reduce_fmms.py diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py new file mode 100644 index 000000000..6fee07f59 --- /dev/null +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -0,0 +1,292 @@ +__copyright__ = "Copyright (C) 2021 Isuru Fernando" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative) + +from pymbolic.mapper import Mapper +from pymbolic.primitives import Product +from pymbolic.interop.sympy import PymbolicToSympyMapper +import sympy + +from collections import defaultdict + + +def reduce_number_of_fmms(int_gs, source_dependent_variables): + """ + Reduce the number of FMMs needed for a system of expressions with + :class:`~pytential.symbolic.primitives.IntG` objects. + + This is done by converting the `IntG` expression to a matrix of polynomials + with d variables corresponding to d dimensions and each polynomial represents + a derivative operator. All the properties of derivative operator that we want + are reflected in the properties of the polynomial including addition, + multiplication and exact polynomial division. + + This matrix is factored into two matrices where the left hand side matrix + represents a transformation at target and the right hand side matrix represents + a transformation at source. + + If the expressions given are not linear, then the input expressions are + returned as is. + + :arg source_dependent_variables: When reducing FMMs, consider only these + variables as dependent on source. + """ + + source_exprs = [] + mapper = ConvertDensityToSourceExprCoeffMap(source_dependent_variables) + matrix = [] + dim = int_gs[0].target_kernel.dim + axis_vars = sympy.symbols(f"_x0:{dim}") + to_sympy = PymbolicToSympyMapper() + + if not _check_int_gs_common(int_gs): + return int_gs + + for int_g in int_gs: + row = [0]*len(source_exprs) + for density, source_kernel in zip(int_g.densities, int_g.source_kernels): + try: + d = mapper(density) + except ImportError: + return int_gs + for source_expr, coeff in d.items(): + if source_expr not in source_exprs: + source_exprs.append(source_expr) + row += [0] + poly = _convert_kernel_to_poly(source_kernel, axis_vars) + row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) + matrix.append(row) + + for row in matrix: + row += [0]*(len(source_exprs) - len(row)) + + mat = sympy.Matrix(matrix) + lhs_mat = _factor_left(mat, axis_vars) + rhs_mat = _factor_right(mat, lhs_mat) + + if rhs_mat.shape[0] >= mat.shape[0]: + return int_gs + + rhs_mat = rhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) + lhs_mat = lhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) + + base_kernel = int_gs[0].source_kernels[0].get_base_kernel() + base_int_g = int_gs[0].copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)) + rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) + for poly in row] for row in rhs_mat.tolist()] + + rhs_int_gs = [] + for i in range(rhs_mat.shape[0]): + source_kernels = [] + densities = [] + for j in range(rhs_mat.shape[1]): + new_densities = [density * source_exprs[j] for density in + rhs_mat_int_gs[i][j].densities] + source_kernels.extend(rhs_mat_int_gs[i][j].source_kernels) + densities.extend(new_densities) + rhs_int_gs.append(rhs_mat_int_gs[i][0].copy( + source_kernels=tuple(source_kernels), densities=tuple(densities))) + + res = [0]*lhs_mat.shape[0] + for i in range(lhs_mat.shape[0]): + for j in range(lhs_mat.shape[1]): + res[i] += _convert_target_poly_to_int_g(lhs_mat[i, j], + int_gs[i], rhs_int_gs[j]) + + return res + + +def _check_int_gs_common(int_gs): + base_kernel = int_gs[0].source_kernels[0].get_base_kernel() + common_int_g = int_gs[0].copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)) + for int_g in int_gs: + for source_kernel in int_g.source_kernels: + if source_kernel.get_base_kernel() != base_kernel: + return False + if common_int_g != int_g.copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)): + return False + return True + + +def _convert_kernel_to_poly(kernel, axis_vars): + if isinstance(kernel, AxisTargetDerivative): + poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) + return axis_vars[kernel.axis]*poly + elif isinstance(kernel, AxisSourceDerivative): + poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) + return -axis_vars[kernel.axis]*poly + return 1 + + +def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): + from pymbolic.interop.sympy import SympyToPymbolicMapper + to_pymbolic = SympyToPymbolicMapper() + + orig_kernel = orig_int_g.source_kernels[0] + source_kernels = [] + densities = [] + for monom, coeff in poly.terms(): + kernel = orig_kernel + for idim, rep in enumerate(monom): + for _ in range(rep): + kernel = AxisSourceDerivative(idim, kernel) + source_kernels.append(kernel) + densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) + return orig_int_g.copy(source_kernels=tuple(source_kernels), + densities=tuple(densities)) + + +def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): + from pymbolic.interop.sympy import SympyToPymbolicMapper + to_pymbolic = SympyToPymbolicMapper() + + result = 0 + for monom, coeff in poly.terms(): + kernel = orig_int_g.target_kernel + for idim, rep in enumerate(monom): + for _ in range(rep): + kernel = AxisTargetDerivative(idim, kernel) + result += orig_int_g.copy(target_kernel=kernel, + source_kernels=rhs_int_g.source_kernels, + densities=rhs_int_g.densities) * to_pymbolic(coeff) + + return result + + +class ConvertDensityToSourceExprCoeffMap(Mapper): + def __init__(self, source_dependent_variables): + self.source_dependent_variables = source_dependent_variables + + def __call__(self, expr): + if expr in self.source_dependent_variables: + return {expr: 1} + try: + return super().__call__(expr) + except NotImplementedError: + return {1: expr} + + rec = __call__ + + def map_sum(self, expr): + d = defaultdict(lambda: 0) + for child in expr.children: + d_child = self.rec(child) + for k, v in d_child.items(): + d[k] += v + return dict(d) + + def map_product(self, expr): + if len(expr.children) > 2: + left = Product(tuple(expr.children[:2])) + right = Product(tuple(expr.children[2:])) + new_prod = Product((left, right)) + return self.rec(new_prod) + elif len(expr.children) == 1: + return self.rec(expr.children[0]) + elif len(expr.children) == 0: + return {1: 1} + left, right = expr.children + d_left = self.rec(left) + d_right = self.rec(right) + d = defaultdict(lambda: 0) + for k_left, v_left in d_left.items(): + for k_right, v_right in d_right.items(): + d[k_left*k_right] += v_left*v_right + return dict(d) + + def map_power(self, expr): + d_base = self.rec(expr.base) + d_exponent = self.rec(expr.exponent) + if len(d_exponent) > 1: + raise ValueError + exp_k, exp_v = list(d_exponent.items())[0] + if exp_k != 1: + raise ValueError + for k in d_base.keys(): + d_base[k] = d_base[k]**exp_v + return d_base + + def map_quotient(self, expr): + d_num = self.rec(expr.numerator) + d_den = self.rec(expr.denominator) + if len(d_den) > 1: + raise ValueError + den_k, den_v = list(d_den.items())[0] + if den_k != 1: + raise ValueError + for k in d_num.keys(): + d_num[k] /= den_v + return d_num + + +def _syzygy_module(m, gens): + import sympy + from sympy.polys.orderings import grevlex + + def _convert_to_matrix(module, *gens): + import sympy + result = [] + for syzygy in module: + row = [] + for dmp in syzygy.data: + row.append(sympy.Poly(dmp.to_dict(), *gens, + domain=sympy.EX).as_expr()) + result.append(row) + return sympy.Matrix(result) + + ring = sympy.EX.old_poly_ring(*gens, order=grevlex) + column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist(), order=grevlex) + for i in range(m.shape[1])] + column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] + + intersection = column_syzygy_modules[0] + for i in range(1, len(column_syzygy_modules)): + intersection = intersection.intersect(column_syzygy_modules[i]) + + m2 = intersection._groebner_vec() + m3 = _convert_to_matrix(m2, *gens) + return m3 + + +def _factor_left(mat, axis_vars): + return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T + + +def _factor_right(mat, factor_left): + import sympy + ys = sympy.symbols(f"_y{{0:{factor_left.shape[1]}}}") + factor_right = [] + for i in range(mat.shape[1]): + aug_mat = sympy.zeros(factor_left.shape[0], factor_left.shape[1] + 1) + aug_mat[:, :factor_left.shape[1]] = factor_left + aug_mat[:, factor_left.shape[1]] = mat[:, i] + res_map = sympy.solve_linear_system(aug_mat, *ys) + row = [] + for y in ys: + row.append(res_map[y]) + factor_right.append(row) + factor_right = sympy.Matrix(factor_right).T + return factor_right diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 58de057ab..df86256b5 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -30,12 +30,12 @@ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) -from pymbolic.mapper import WalkMapper, Mapper +from pymbolic.mapper import WalkMapper from pymbolic.primitives import Sum, Product, Quotient from pytential.symbolic.primitives import IntG, NodeCoordinateComponent import pytential -from collections import defaultdict +from .reduce_fmms import reduce_number_of_fmms def _chop(expr, tol): @@ -338,7 +338,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, result_int_gs.append(0) if source_dependent_variables is not None: - result_int_gs = _reduce_number_of_fmms(result_int_gs, + result_int_gs = reduce_number_of_fmms(result_int_gs, source_dependent_variables) result = [coeff + int_g for coeff, int_g in zip(result_coeffs, result_int_gs)] return np.array(result, dtype=object) @@ -389,204 +389,6 @@ def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): return result -def _syzygy_module(m, gens): - import sympy - from sympy.polys.orderings import grevlex - - def _convert_to_matrix(module, *gens): - import sympy - result = [] - for syzygy in module: - row = [] - for dmp in syzygy.data: - row.append(sympy.Poly(dmp.to_dict(), *gens, - domain=sympy.EX).as_expr()) - result.append(row) - return sympy.Matrix(result) - - ring = sympy.EX.old_poly_ring(*gens, order=grevlex) - column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist(), order=grevlex) - for i in range(m.shape[1])] - column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] - - intersection = column_syzygy_modules[0] - for i in range(1, len(column_syzygy_modules)): - intersection = intersection.intersect(column_syzygy_modules[i]) - - m2 = intersection._groebner_vec() - m3 = _convert_to_matrix(m2, *gens) - return m3 - - -def _factor_left(mat, axis_vars): - return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T - - -def _factor_right(mat, factor_left): - import sympy - ys = sympy.symbols(f"_y{{0:{factor_left.shape[1]}}}") - factor_right = [] - for i in range(mat.shape[1]): - aug_mat = sympy.zeros(factor_left.shape[0], factor_left.shape[1] + 1) - aug_mat[:, :factor_left.shape[1]] = factor_left - aug_mat[:, factor_left.shape[1]] = mat[:, i] - res_map = sympy.solve_linear_system(aug_mat, *ys) - row = [] - for y in ys: - row.append(res_map[y]) - factor_right.append(row) - factor_right = sympy.Matrix(factor_right).T - return factor_right - - -def _check_int_gs_common(int_gs): - base_kernel = int_gs[0].source_kernels[0].get_base_kernel() - common_int_g = int_gs[0].copy(target_kernel=base_kernel, - source_kernels=(base_kernel,), densities=(1,)) - for int_g in int_gs: - for source_kernel in int_g.source_kernels: - if source_kernel.get_base_kernel() != base_kernel: - return False - if common_int_g != int_g.copy(target_kernel=base_kernel, - source_kernels=(base_kernel,), densities=(1,)): - return False - return True - - -def _reduce_number_of_fmms(int_gs, source_dependent_variables): - from pymbolic.interop.sympy import PymbolicToSympyMapper - import sympy - - source_exprs = [] - mapper = ConvertDensityToSourceExprCoeffMap(source_dependent_variables) - matrix = [] - dim = int_gs[0].target_kernel.dim - axis_vars = sympy.symbols(f"_x0:{dim}") - to_sympy = PymbolicToSympyMapper() - - _check_int_gs_common(int_gs) - - for int_g in int_gs: - row = [0]*len(source_exprs) - for density, source_kernel in zip(int_g.densities, int_g.source_kernels): - try: - d = mapper(density) - except ImportError: - return int_gs - for source_expr, coeff in d.items(): - if source_expr not in source_exprs: - source_exprs.append(source_expr) - row += [0] - poly = _convert_kernel_to_poly(source_kernel, axis_vars) - row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) - matrix.append(row) - - for row in matrix: - row += [0]*(len(source_exprs) - len(row)) - - mat = sympy.Matrix(matrix) - lhs_mat = _factor_left(mat, axis_vars) - rhs_mat = _factor_right(mat, lhs_mat) - - if rhs_mat.shape[0] >= mat.shape[0]: - return int_gs - - rhs_mat = rhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) - lhs_mat = lhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) - - base_kernel = int_gs[0].source_kernels[0].get_base_kernel() - base_int_g = int_gs[0].copy(target_kernel=base_kernel, - source_kernels=(base_kernel,), densities=(1,)) - rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) - for poly in row] for row in rhs_mat.tolist()] - - rhs_int_gs = [] - for i in range(rhs_mat.shape[0]): - source_kernels = [] - densities = [] - for j in range(rhs_mat.shape[1]): - new_densities = [density * source_exprs[j] for density in - rhs_mat_int_gs[i][j].densities] - source_kernels.extend(rhs_mat_int_gs[i][j].source_kernels) - densities.extend(new_densities) - rhs_int_gs.append(rhs_mat_int_gs[i][0].copy( - source_kernels=tuple(source_kernels), densities=tuple(densities))) - - res = [0]*lhs_mat.shape[0] - for i in range(lhs_mat.shape[0]): - for j in range(lhs_mat.shape[1]): - res[i] += _convert_target_poly_to_int_g(lhs_mat[i, j], - int_gs[i], rhs_int_gs[j]) - - return res - - -class ConvertDensityToSourceExprCoeffMap(Mapper): - def __init__(self, source_dependent_variables): - self.source_dependent_variables = source_dependent_variables - - def __call__(self, expr): - if expr in self.source_dependent_variables: - return {expr: 1} - try: - return super().__call__(expr) - except NotImplementedError: - return {1: expr} - - rec = __call__ - - def map_sum(self, expr): - d = defaultdict(lambda: 0) - for child in expr.children: - d_child = self.rec(child) - for k, v in d_child.items(): - d[k] += v - return dict(d) - - def map_product(self, expr): - if len(expr.children) > 2: - left = Product(tuple(expr.children[:2])) - right = Product(tuple(expr.children[2:])) - new_prod = Product((left, right)) - return self.rec(new_prod) - elif len(expr.children) == 1: - return self.rec(expr.children[0]) - elif len(expr.children) == 0: - return {1: 1} - left, right = expr.children - d_left = self.rec(left) - d_right = self.rec(right) - d = defaultdict(lambda: 0) - for k_left, v_left in d_left.items(): - for k_right, v_right in d_right.items(): - d[k_left*k_right] += v_left*v_right - return dict(d) - - def map_power(self, expr): - d_base = self.rec(expr.base) - d_exponent = self.rec(expr.exponent) - if len(d_exponent) > 1: - raise ValueError - exp_k, exp_v = list(d_exponent.items())[0] - if exp_k != 1: - raise ValueError - for k in d_base.keys(): - d_base[k] = d_base[k]**exp_v - return d_base - - def map_quotient(self, expr): - d_num = self.rec(expr.numerator) - d_den = self.rec(expr.denominator) - if len(d_den) > 1: - raise ValueError - den_k, den_v = list(d_den.items())[0] - if den_k != 1: - raise ValueError - for k in d_num.keys(): - d_num[k] /= den_v - return d_num - - def _convert_target_multiplier_to_source(int_g): from sumpy.symbolic import SympyToPymbolicMapper tgt_knl = int_g.target_kernel From fefa5632e6a149e4a64a307ec9f514cd13eb8c9e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 26 Aug 2021 14:51:33 -0500 Subject: [PATCH 085/209] document more functions in reduce_fmms --- pytential/symbolic/pde/reduce_fmms.py | 40 +++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 6fee07f59..e8fb0a9a3 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -24,12 +24,17 @@ from pymbolic.mapper import Mapper from pymbolic.primitives import Product -from pymbolic.interop.sympy import PymbolicToSympyMapper +from pymbolic.interop.sympy import PymbolicToSympyMapper, SympyToPymbolicMapper import sympy from collections import defaultdict +__all__ = ( + "reduce_number_of_fmms", + ) + + def reduce_number_of_fmms(int_gs, source_dependent_variables): """ Reduce the number of FMMs needed for a system of expressions with @@ -118,6 +123,10 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): def _check_int_gs_common(int_gs): + """Checks that the :class:`~pytential.symbolic.primtive.IntG` objects + have the same base kernel and other properties that would allow + merging them. + """ base_kernel = int_gs[0].source_kernels[0].get_base_kernel() common_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) @@ -132,6 +141,14 @@ def _check_int_gs_common(int_gs): def _convert_kernel_to_poly(kernel, axis_vars): + """Converts a :class:`sumpy.kernel.Kernel` object to a polynomial. + A :class:`sumpy.kernel.Kernel` represents a derivative operator + and the derivative operator is converted to a polynomial with + variables given by `axis_vars`. + + For eg: for target y and source x the derivative operator, + d/dx_1 dy_2 + d/dy_1 is converted to -y_2 * y_1 + y_1. + """ if isinstance(kernel, AxisTargetDerivative): poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) return axis_vars[kernel.axis]*poly @@ -142,7 +159,11 @@ def _convert_kernel_to_poly(kernel, axis_vars): def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): - from pymbolic.interop.sympy import SympyToPymbolicMapper + """This does the opposite of :func:`_convert_kernel_to_poly` + and converts a polynomial back to a source derivative + operator. First it is converted to a :class:`sumpy.kernel.Kernel` + and then to a :class:`~pytential.symbolic.primitives.IntG`. + """ to_pymbolic = SympyToPymbolicMapper() orig_kernel = orig_int_g.source_kernels[0] @@ -160,7 +181,11 @@ def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): - from pymbolic.interop.sympy import SympyToPymbolicMapper + """This does the opposite of :func:`_convert_kernel_to_poly` + and converts a polynomial back to a target derivative + operator. It is applied to a :class:`~pytential.symbolic.primitives.IntG` + object and returns a new instance. + """ to_pymbolic = SympyToPymbolicMapper() result = 0 @@ -243,11 +268,13 @@ def map_quotient(self, expr): def _syzygy_module(m, gens): - import sympy + """Takes as input a module of polynomials with domain `sympy.EX` represented + as a matrix and returns the syzygy module as a matrix of polynomials in the + same domain. + """ from sympy.polys.orderings import grevlex def _convert_to_matrix(module, *gens): - import sympy result = [] for syzygy in module: row = [] @@ -272,11 +299,12 @@ def _convert_to_matrix(module, *gens): def _factor_left(mat, axis_vars): + """Return the left hand side of the factorisation of the matrix""" return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T def _factor_right(mat, factor_left): - import sympy + """Return the right hand side of the factorisation of the matrix""" ys = sympy.symbols(f"_y{{0:{factor_left.shape[1]}}}") factor_right = [] for i in range(mat.shape[1]): From 4391a9e1a96201f280100e4df35bbfdb287c7d40 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 27 Aug 2021 14:57:43 -0500 Subject: [PATCH 086/209] move chop and lu_solve_with_expand to utils --- pytential/symbolic/pde/system_utils.py | 211 ++++++++++--------------- pytential/utils.py | 61 ++++++- 2 files changed, 147 insertions(+), 125 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index df86256b5..45cbae015 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -33,23 +33,96 @@ from pymbolic.mapper import WalkMapper from pymbolic.primitives import Sum, Product, Quotient from pytential.symbolic.primitives import IntG, NodeCoordinateComponent +from pytential.utils import chop, lu_solve_with_expand import pytential from .reduce_fmms import reduce_number_of_fmms +__all__ = ( + "merge_int_g_exprs", + "get_deriv_relation", + ) -def _chop(expr, tol): - nums = expr.atoms(sym.Number) - replace_dict = {} - for num in nums: - if float(abs(num)) < tol: - replace_dict[num] = 0 - else: - new_num = float(num) - if abs((int(new_num) - new_num)/new_num) < tol: - new_num = int(new_num) - replace_dict[num] = new_num - return expr.xreplace(replace_dict) + +def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, + source_dependent_variables=None): + """ + Merge expressions involving :class:`~pytential.symbolic.primitives.IntG` + objects. + + Several techniques are used for merging and reducing number of FMMs + + * When `base_kernel` is given an `IntG` is rewritten using `base_kernel` + and its derivatives. + + * :class:`sumpy.kernel.AxisTargetDerivative` instances are converted + to :class:`sumpy.kernel.AxisSourceDerivative` instances. + + * If there is a sum of two `IntG`s with same target derivative and different + source derivatives of the same kernel, they are merged into one FMM. + + * If possible, convert :class:`sumpy.kernel.AxisSourceDerivative` to + :class:`sumpy.kernel.DirectionalSourceDerivative`. + + * Reduce the number of FMMs by converting the `IntG` expression to + a matrix and factoring the matrix where the left operand matrix represents + a transformation at target and the right matrix represents a transformation + at source. For this to work, we need to know which variables depend on + source so that they do not end up in the left operand. User needs to supply + this as the argument `source_dependent_variable`. + + :arg base_kernel: A :class:`sumpy.kernel.Kernel` object if given will be used + for converting a :class:`~pytential.symbolic.primitives.IntG` to a linear + expression of same type with the kernel replaced by base_kernel and its + derivatives + + :arg verbose: increase verbosity of merging + + :arg source_dependent_variable: When merging expressions, consider only these + variables as dependent on source. Otherwise consider all variables + as source dependent. This is important when reducing the number of FMMs + needed for the output. + """ + replacements = {} + + if base_kernel is not None: + mapper = GetIntGs() + [mapper(expr) for expr in exprs] + int_g_s = mapper.int_g_s + for int_g in int_g_s: + new_int_g = _convert_target_deriv_to_source(int_g) + tgt_knl = new_int_g.target_kernel + if isinstance(tgt_knl, TargetPointMultiplier) \ + and not isinstance(tgt_knl.inner_kernel, KernelWrapper): + new_int_g_s = _convert_target_multiplier_to_source(new_int_g) + else: + new_int_g_s = [new_int_g] + replacements[int_g] = sum(convert_int_g_to_base(new_int_g, + base_kernel, verbose=verbose) for new_int_g in new_int_g_s) + + result_coeffs = [] + result_int_gs = [] + + for expr in exprs: + if not have_int_g_s(expr): + result_coeffs.append(expr) + result_int_gs.append(0) + try: + result_coeff, result_int_g = _merge_int_g_expr(expr, replacements) + result_int_g = _convert_axis_source_to_directional_source(result_int_g) + result_int_g = result_int_g.copy( + densities=_simplify_densities(result_int_g.densities)) + result_coeffs.append(result_coeff) + result_int_gs.append(result_int_g) + except AssertionError: + result_coeffs.append(expr) + result_int_gs.append(0) + + if source_dependent_variables is not None: + result_int_gs = reduce_number_of_fmms(result_int_gs, + source_dependent_variables) + result = [coeff + int_g for coeff, int_g in zip(result_coeffs, result_int_gs)] + return np.array(result, dtype=object) def _n(expr): @@ -112,35 +185,6 @@ def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): return (L, U, perm), rand, mis -def _LUsolve_with_expand(L, U, perm, b): - def forward_substitution(L, b): - n = len(b) - res = sym.Matrix(b) - for i in range(n): - for j in range(i): - res[i] -= L[i, j]*res[j] - res[i] = (res[i] / L[i, i]).expand() - return res - - def backward_substitution(U, b): - n = len(b) - res = sym.Matrix(b) - for i in range(n-1, -1, -1): - for j in range(n - 1, i, -1): - res[i] -= U[i, j]*res[j] - res[i] = (res[i] / U[i, i]).expand() - return res - - def permuteFwd(b, perm): - res = sym.Matrix(b) - for p, q in perm: - res[p], res[q] = res[q], res[p] - return res - - return backward_substitution(U, - forward_substitution(L, permuteFwd(b, perm))) - - @memoize_on_first_arg def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, verbose=False): @@ -161,9 +205,9 @@ def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, if verbose: print(kernel, end=" = ", flush=True) - sol = _LUsolve_with_expand(L, U, perm, vec) + sol = lu_solve_with_expand(L, U, perm, vec) for i, coeff in enumerate(sol): - coeff = _chop(coeff, tol) + coeff = chop(coeff, tol) if coeff == 0: continue if mis[i] != (-1, -1, -1): @@ -263,87 +307,6 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): return result -def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, - source_dependent_variables=None): - """ - Merge expressions involving :class:`~pytential.symbolic.primitives.IntG` - objects. - - Several techniques are used for merging and reducing number of FMMs - - * When `base_kernel` is given an `IntG` is rewritten using `base_kernel` - and its derivatives. - - * :class:`sumpy.kernel.AxisTargetDerivative` instances are converted - to :class:`sumpy.kernel.AxisSourceDerivative` instances. - - * If there is a sum of two `IntG`s with same target derivative and different - source derivatives of the same kernel, they are merged into one FMM. - - * If possible, convert :class:`sumpy.kernel.AxisSourceDerivative` to - :class:`sumpy.kernel.DirectionalSourceDerivative`. - - * Reduce the number of FMMs by converting the `IntG` expression to - a matrix and factoring the matrix where the left operand matrix represents - a transformation at target and the right matrix represents a transformation - at source. For this to work, we need to know which variables depend on - source so that they do not end up in the left operand. User needs to supply - this as the argument `source_dependent_variable`. - - :arg base_kernel: A :class:`sumpy.kernel.Kernel` object if given will be used - for converting a :class:`~pytential.symbolic.primitives.IntG` to a linear - expression of same type with the kernel replaced by base_kernel and its - derivatives - - :arg verbose: increase verbosity of merging - - :arg source_dependent_variable: When merging expressions, consider only these - variables as dependent on source. Otherwise consider all variables - as source dependent. This is important when reducing the number of FMMs - needed for the output. - """ - replacements = {} - - if base_kernel is not None: - mapper = GetIntGs() - [mapper(expr) for expr in exprs] - int_g_s = mapper.int_g_s - for int_g in int_g_s: - new_int_g = _convert_target_deriv_to_source(int_g) - tgt_knl = new_int_g.target_kernel - if isinstance(tgt_knl, TargetPointMultiplier) \ - and not isinstance(tgt_knl.inner_kernel, KernelWrapper): - new_int_g_s = _convert_target_multiplier_to_source(new_int_g) - else: - new_int_g_s = [new_int_g] - replacements[int_g] = sum(convert_int_g_to_base(new_int_g, - base_kernel, verbose=verbose) for new_int_g in new_int_g_s) - - result_coeffs = [] - result_int_gs = [] - - for expr in exprs: - if not have_int_g_s(expr): - result_coeffs.append(expr) - result_int_gs.append(0) - try: - result_coeff, result_int_g = _merge_int_g_expr(expr, replacements) - result_int_g = _convert_axis_source_to_directional_source(result_int_g) - result_int_g = result_int_g.copy( - densities=_simplify_densities(result_int_g.densities)) - result_coeffs.append(result_coeff) - result_int_gs.append(result_int_g) - except AssertionError: - result_coeffs.append(expr) - result_int_gs.append(0) - - if source_dependent_variables is not None: - result_int_gs = reduce_number_of_fmms(result_int_gs, - source_dependent_variables) - result = [coeff + int_g for coeff, int_g in zip(result_coeffs, result_int_gs)] - return np.array(result, dtype=object) - - def _convert_kernel_to_poly(kernel, axis_vars): if isinstance(kernel, AxisTargetDerivative): poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) diff --git a/pytential/utils.py b/pytential/utils.py index b64176909..64c369ba4 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -1,5 +1,5 @@ __copyright__ = """ -Copyright (C) 2020 Matt Wala +Copyright (C) 2020 Isuru Fernando """ __license__ = """ @@ -22,6 +22,8 @@ THE SOFTWARE. """ +import sumpy.symbolic as sym + def sort_arrays_together(*arys, key=None): """Sort a sequence of arrays by considering them @@ -32,4 +34,61 @@ def sort_arrays_together(*arys, key=None): """ return zip(*sorted([x for x in zip(*arys)], key=key)) + +def chop(expr, tol): + """Given a symbolic expression, remove all occurences of numbers + with absolute value less than a given tolerance and replace floating + point numbers that are close to an integer up to a given relative + tolerance by the integer. + """ + nums = expr.atoms(sym.Number) + replace_dict = {} + for num in nums: + if float(abs(num)) < tol: + replace_dict[num] = 0 + else: + new_num = float(num) + if abs((int(new_num) - new_num)/new_num) < tol: + new_num = int(new_num) + replace_dict[num] = new_num + return expr.xreplace(replace_dict) + + +def lu_solve_with_expand(L, U, perm, b): + """Given an LU factorization and a vector, solve a linear + system with intermediate results expanded to avoid + an explosion of the expression trees + + :param L: lower triangular matrix + :param U: upper triangular matrix + :param perm: permutation matrix + :param b: column vector to solve for + """ + def forward_substitution(L, b): + n = len(b) + res = sym.Matrix(b) + for i in range(n): + for j in range(i): + res[i] -= L[i, j]*res[j] + res[i] = (res[i] / L[i, i]).expand() + return res + + def backward_substitution(U, b): + n = len(b) + res = sym.Matrix(b) + for i in range(n-1, -1, -1): + for j in range(n - 1, i, -1): + res[i] -= U[i, j]*res[j] + res[i] = (res[i] / U[i, i]).expand() + return res + + def permuteFwd(b, perm): + res = sym.Matrix(b) + for p, q in perm: + res[p], res[q] = res[q], res[p] + return res + + return backward_substitution(U, + forward_substitution(L, permuteFwd(b, perm))) + # vim: foldmethod=marker From 71d15401611400bf47c5d27d99dff394e3dee0b3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 09:26:21 -0500 Subject: [PATCH 087/209] Use coefficientcollector from pymbolic --- pytential/symbolic/pde/reduce_fmms.py | 66 ++++----------------------- 1 file changed, 9 insertions(+), 57 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index e8fb0a9a3..e11965496 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -25,6 +25,8 @@ from pymbolic.mapper import Mapper from pymbolic.primitives import Product from pymbolic.interop.sympy import PymbolicToSympyMapper, SympyToPymbolicMapper +from pymbolic.mapper.coefficient import ( + CoefficientCollector as CoefficientCollectorBase) import sympy from collections import defaultdict @@ -58,7 +60,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): """ source_exprs = [] - mapper = ConvertDensityToSourceExprCoeffMap(source_dependent_variables) + mapper = CoefficientCollector(source_dependent_variables) matrix = [] dim = int_gs[0].target_kernel.dim axis_vars = sympy.symbols(f"_x0:{dim}") @@ -201,71 +203,21 @@ def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): return result -class ConvertDensityToSourceExprCoeffMap(Mapper): +class CoefficientCollector(CoefficientCollectorBase): + """This extends :class:`pymbolic.mapper.coefficient.CoefficientCollector` + by extending the targets to be any expression instead of just algebraic + leafs + """ def __init__(self, source_dependent_variables): self.source_dependent_variables = source_dependent_variables def __call__(self, expr): if expr in self.source_dependent_variables: return {expr: 1} - try: - return super().__call__(expr) - except NotImplementedError: - return {1: expr} + return super().__call__(expr) rec = __call__ - def map_sum(self, expr): - d = defaultdict(lambda: 0) - for child in expr.children: - d_child = self.rec(child) - for k, v in d_child.items(): - d[k] += v - return dict(d) - - def map_product(self, expr): - if len(expr.children) > 2: - left = Product(tuple(expr.children[:2])) - right = Product(tuple(expr.children[2:])) - new_prod = Product((left, right)) - return self.rec(new_prod) - elif len(expr.children) == 1: - return self.rec(expr.children[0]) - elif len(expr.children) == 0: - return {1: 1} - left, right = expr.children - d_left = self.rec(left) - d_right = self.rec(right) - d = defaultdict(lambda: 0) - for k_left, v_left in d_left.items(): - for k_right, v_right in d_right.items(): - d[k_left*k_right] += v_left*v_right - return dict(d) - - def map_power(self, expr): - d_base = self.rec(expr.base) - d_exponent = self.rec(expr.exponent) - if len(d_exponent) > 1: - raise ValueError - exp_k, exp_v = list(d_exponent.items())[0] - if exp_k != 1: - raise ValueError - for k in d_base.keys(): - d_base[k] = d_base[k]**exp_v - return d_base - - def map_quotient(self, expr): - d_num = self.rec(expr.numerator) - d_den = self.rec(expr.denominator) - if len(d_den) > 1: - raise ValueError - den_k, den_v = list(d_den.items())[0] - if den_k != 1: - raise ValueError - for k in d_num.keys(): - d_num[k] /= den_v - return d_num - def _syzygy_module(m, gens): """Takes as input a module of polynomials with domain `sympy.EX` represented From 5d3a22dd3fbdf5f1bc8c5b0ac0f5192c204618e6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 09:28:22 -0500 Subject: [PATCH 088/209] improve the docstring of reduce_number_of_fmms --- pytential/symbolic/pde/reduce_fmms.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index e11965496..a71693fab 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -42,21 +42,24 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): Reduce the number of FMMs needed for a system of expressions with :class:`~pytential.symbolic.primitives.IntG` objects. - This is done by converting the `IntG` expression to a matrix of polynomials + This is done by converting the ``IntG`` expression to a matrix of polynomials with d variables corresponding to d dimensions and each polynomial represents a derivative operator. All the properties of derivative operator that we want are reflected in the properties of the polynomial including addition, multiplication and exact polynomial division. This matrix is factored into two matrices where the left hand side matrix - represents a transformation at target and the right hand side matrix represents - a transformation at source. + represents a transformation at the target and the right hand side matrix + represents a transformation at the source. If the expressions given are not linear, then the input expressions are returned as is. :arg source_dependent_variables: When reducing FMMs, consider only these - variables as dependent on source. + variables as dependent on source. For eg: densities, source + derivative vectors. Note that there's no analogous argument for target + as the algorithm assumes that there are no target dependent variables + passed to this function. """ source_exprs = [] From 7d158304bfef8dd6d6253da0c79f55c7f4706c2d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 09:56:51 -0500 Subject: [PATCH 089/209] create new helper function for creating the matrix --- pytential/symbolic/pde/reduce_fmms.py | 69 ++++++++++++++++++--------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index a71693fab..10d2ef56c 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -62,50 +62,39 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): passed to this function. """ - source_exprs = [] - mapper = CoefficientCollector(source_dependent_variables) - matrix = [] dim = int_gs[0].target_kernel.dim axis_vars = sympy.symbols(f"_x0:{dim}") - to_sympy = PymbolicToSympyMapper() if not _check_int_gs_common(int_gs): return int_gs - for int_g in int_gs: - row = [0]*len(source_exprs) - for density, source_kernel in zip(int_g.densities, int_g.source_kernels): - try: - d = mapper(density) - except ImportError: - return int_gs - for source_expr, coeff in d.items(): - if source_expr not in source_exprs: - source_exprs.append(source_expr) - row += [0] - poly = _convert_kernel_to_poly(source_kernel, axis_vars) - row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) - matrix.append(row) - - for row in matrix: - row += [0]*(len(source_exprs) - len(row)) + try: + mat, source_exprs = _create_matrix(int_gs, source_dependent_variables, + axis_vars) + except ValueError: + return int_gs - mat = sympy.Matrix(matrix) + mat = sympy.Matrix(mat) + # Factor the matrix into two lhs_mat = _factor_left(mat, axis_vars) rhs_mat = _factor_right(mat, lhs_mat) if rhs_mat.shape[0] >= mat.shape[0]: return int_gs + # Create SymPy Polynomial objects rhs_mat = rhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) lhs_mat = lhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) base_kernel = int_gs[0].source_kernels[0].get_base_kernel() base_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) + + # Convert each element in the RHS matrix to IntGs rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) for poly in row] for row in rhs_mat.tolist()] + # For each row in the RHS matrix, merge the IntGs to one IntG rhs_int_gs = [] for i in range(rhs_mat.shape[0]): source_kernels = [] @@ -118,6 +107,8 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): rhs_int_gs.append(rhs_mat_int_gs[i][0].copy( source_kernels=tuple(source_kernels), densities=tuple(densities))) + # Now that we have the IntG expressions depending on the source + # we now have to attach the target dependent derivatives. res = [0]*lhs_mat.shape[0] for i in range(lhs_mat.shape[0]): for j in range(lhs_mat.shape[1]): @@ -127,6 +118,40 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): return res +def _create_matrix(int_gs, source_dependent_variables, axis_vars): + """Create a matrix from a list of expression with + :class:`~pytential.symbolic.primitives.IntG` objects and returns + the matrix and the expressions corresponding to each column. + Each expression is one of ``source_dependent_variables`` or + equals to one. Each element in the matrix is a multi-variate polynomial + and the variables in the polynomial are from ``axis_vars`` input. + Each polynomial represents a derivative operator. + """ + source_exprs = [] + coefficient_collector = CoefficientCollector(source_dependent_variables) + to_sympy = PymbolicToSympyMapper() + + for int_g in int_gs: + row = [0]*len(source_exprs) + for density, source_kernel in zip(int_g.densities, int_g.source_kernels): + d = coefficient_collector(density) + for source_expr, coeff in d.items(): + if source_expr not in source_exprs: + source_exprs.append(source_expr) + row += [0] + poly = _convert_kernel_to_poly(source_kernel, axis_vars) + row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) + matrix.append(row) + + # At the beginning, we didn't know the number of columns of the matrix. + # Therefore we used a list for rows and they kept expanding. + # Here we are adding zero padding to make the result look like a matrix. + for row in matrix: + row += [0]*(len(source_exprs) - len(row)) + + return matrix, source_exprs + + def _check_int_gs_common(int_gs): """Checks that the :class:`~pytential.symbolic.primtive.IntG` objects have the same base kernel and other properties that would allow From c4068e901e5eba1463f76f9b69878dcf660c247e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 10:15:39 -0500 Subject: [PATCH 090/209] Docs for data types of int_gs and source_dependent_variables and also document that there's no target_dependent_variables --- pytential/symbolic/pde/reduce_fmms.py | 31 ++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 10d2ef56c..0f5ae6b8f 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -39,10 +39,10 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): """ - Reduce the number of FMMs needed for a system of expressions with + Reduce the number of FMMs needed for a system of :class:`~pytential.symbolic.primitives.IntG` objects. - This is done by converting the ``IntG`` expression to a matrix of polynomials + This is done by converting the ``IntG`` object to a matrix of polynomials with d variables corresponding to d dimensions and each polynomial represents a derivative operator. All the properties of derivative operator that we want are reflected in the properties of the polynomial including addition, @@ -55,11 +55,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): If the expressions given are not linear, then the input expressions are returned as is. - :arg source_dependent_variables: When reducing FMMs, consider only these - variables as dependent on source. For eg: densities, source - derivative vectors. Note that there's no analogous argument for target - as the algorithm assumes that there are no target dependent variables - passed to this function. + :arg int_gs: list of ``IntG`` objects. + + :arg source_dependent_variables: list of :class:`pymbolic.primtives.Expression` + objects. When reducing FMMs, consider only these variables as dependent + on source. For eg: densities, source derivative vectors. + + Note: there is no argument for target dependent variables as the algorithm + assumes that there are no target dependent variables passed to this function. """ dim = int_gs[0].target_kernel.dim @@ -79,6 +82,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): lhs_mat = _factor_left(mat, axis_vars) rhs_mat = _factor_right(mat, lhs_mat) + # If there are n inputs and m outputs, + # + # - matrix = R^{m x n}, + # - LHS = R^{m x k}, + # - RHS = R^{k x n}. + # + # If k is not greater than or equal to n we are gaining nothing. + # Return as is. if rhs_mat.shape[0] >= mat.shape[0]: return int_gs @@ -95,6 +106,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): for poly in row] for row in rhs_mat.tolist()] # For each row in the RHS matrix, merge the IntGs to one IntG + # to get a total of k IntGs. rhs_int_gs = [] for i in range(rhs_mat.shape[0]): source_kernels = [] @@ -119,9 +131,8 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): def _create_matrix(int_gs, source_dependent_variables, axis_vars): - """Create a matrix from a list of expression with - :class:`~pytential.symbolic.primitives.IntG` objects and returns - the matrix and the expressions corresponding to each column. + """Create a matrix from a list of :class:`~pytential.symbolic.primitives.IntG` + objects and returns the matrix and the expressions corresponding to each column. Each expression is one of ``source_dependent_variables`` or equals to one. Each element in the matrix is a multi-variate polynomial and the variables in the polynomial are from ``axis_vars`` input. From 391eec6ec640fc0c28d5faef93c334cc9b4a0fa8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 11:55:11 -0500 Subject: [PATCH 091/209] Fix flake8 failures --- pytential/symbolic/pde/reduce_fmms.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 0f5ae6b8f..3a37a1b93 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -22,15 +22,11 @@ from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative) -from pymbolic.mapper import Mapper -from pymbolic.primitives import Product from pymbolic.interop.sympy import PymbolicToSympyMapper, SympyToPymbolicMapper from pymbolic.mapper.coefficient import ( CoefficientCollector as CoefficientCollectorBase) import sympy -from collections import defaultdict - __all__ = ( "reduce_number_of_fmms", @@ -141,6 +137,7 @@ def _create_matrix(int_gs, source_dependent_variables, axis_vars): source_exprs = [] coefficient_collector = CoefficientCollector(source_dependent_variables) to_sympy = PymbolicToSympyMapper() + matrix = [] for int_g in int_gs: row = [0]*len(source_exprs) From 86e5882e00899c82dc00e1679226de6bf4d2cd4f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 14:23:33 -0500 Subject: [PATCH 092/209] write a mapper for substitution --- pytential/symbolic/pde/system_utils.py | 65 +++++++++++++++++--------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 45cbae015..43a5e0ee5 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -33,6 +33,7 @@ from pymbolic.mapper import WalkMapper from pymbolic.primitives import Sum, Product, Quotient from pytential.symbolic.primitives import IntG, NodeCoordinateComponent +from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand import pytential @@ -83,23 +84,27 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, as source dependent. This is important when reducing the number of FMMs needed for the output. """ - replacements = {} if base_kernel is not None: - mapper = GetIntGs() - [mapper(expr) for expr in exprs] + get_int_g_mapper = GetIntGs() + [get_int_g_mapper(expr) for expr in exprs] int_g_s = mapper.int_g_s + replacements = {} for int_g in int_g_s: + # First convert IntGs with target derivatives to source derivatives new_int_g = _convert_target_deriv_to_source(int_g) tgt_knl = new_int_g.target_kernel - if isinstance(tgt_knl, TargetPointMultiplier) \ - and not isinstance(tgt_knl.inner_kernel, KernelWrapper): - new_int_g_s = _convert_target_multiplier_to_source(new_int_g) - else: - new_int_g_s = [new_int_g] + # Convert IntGs with TargetMultiplier to a sum of IntGs without + # TargetMultipliers + new_int_g_s = _convert_target_multiplier_to_source(new_int_g) + # Convert IntGs with different kernels to expressions containing + # IntGs with base_kernel or its derivatives replacements[int_g] = sum(convert_int_g_to_base(new_int_g, base_kernel, verbose=verbose) for new_int_g in new_int_g_s) + substitutor = IntGSubstitutor(replacements) + exprs = [subsitutor(expr) for expr in exprs] + result_coeffs = [] result_int_gs = [] @@ -108,8 +113,11 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, result_coeffs.append(expr) result_int_gs.append(0) try: - result_coeff, result_int_g = _merge_int_g_expr(expr, replacements) + # run the main routine to merge IntG expressions with + result_coeff, result_int_g = _merge_int_g_expr(expr) result_int_g = _convert_axis_source_to_directional_source(result_int_g) + # simplify the densities as they may become large due to pymbolic + # not doing automatic simplifications unlike sympy/symengine result_int_g = result_int_g.copy( densities=_simplify_densities(result_int_g.densities)) result_coeffs.append(result_coeff) @@ -125,7 +133,18 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, return np.array(result, dtype=object) -def _n(expr): +class IntGSubstitutor(IdentityMapper): + """Replaces IntGs with pymbolic expression given by the + replacements dictionary + """ + def __init__(self, replacements): + self.replacements = replacements + + def map_int_g(self, expr): + return self.replacements.get(expr, expr) + + +def evalf(expr): from sumpy.symbolic import USE_SYMENGINE if USE_SYMENGINE: # 100 bits @@ -175,7 +194,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): replace_dict = dict( (k, v) for k, v in zip(sym_vec, rand[:, rand_vec_idx]) ) - eval_expr = _n(expr.xreplace(replace_dict)) + eval_expr = evalf(expr.xreplace(replace_dict)) row.append(eval_expr) row.append(1) mat.append(row) @@ -197,7 +216,7 @@ def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, expr = kernel.get_expression(sym_vec) vec = [] for i in range(len(mis)): - vec.append(_n(expr.xreplace(dict((k, v) for + vec.append(evalf(expr.xreplace(dict((k, v) for k, v in zip(sym_vec, rand[:, i]))))) vec = sym.Matrix(vec) result = [] @@ -231,6 +250,9 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, verbose=Fals class GetIntGs(WalkMapper): + """A Mapper that walks expressions and collects + :class:`~pytential.symbolic.primitives.IntG` objects + """ def __init__(self): self.int_g_s = set() @@ -245,6 +267,9 @@ def map_constant(self, expr): def have_int_g_s(expr): + """Checks if a :mod:`pymbolic` expression has + :class:`~pytential.symbolic.primitives.IntG` objects + """ mapper = GetIntGs() mapper(expr) return bool(mapper.int_g_s) @@ -480,12 +505,12 @@ def _simplify_densities(densities): return tuple(result) -def _merge_int_g_expr(expr, replacements): +def _merge_int_g_expr(expr): if isinstance(expr, Sum): result_coeff = 0 result_int_g = 0 for c in expr.children: - coeff, int_g = _merge_int_g_expr(c, replacements) + coeff, int_g = _merge_int_g_expr(c) result_coeff += coeff if int_g == 0: continue @@ -522,19 +547,15 @@ def _merge_int_g_expr(expr, replacements): if not found_int_g: return expr, 0 else: - coeff, new_int_g = _merge_int_g_expr(found_int_g, replacements) + coeff, new_int_g = _merge_int_g_expr(found_int_g) new_densities = (density * mult for density in new_int_g.densities) return coeff*mult, new_int_g.copy(densities=new_densities) elif isinstance(expr, IntG): - new_expr = replacements.get(expr, expr) - if new_expr == expr: - new_int_g = _convert_target_deriv_to_source(expr) - return 0, new_int_g - else: - return _merge_int_g_expr(new_expr, replacements) + new_int_g = _convert_target_deriv_to_source(expr) + return 0, new_int_g elif isinstance(expr, Quotient): mult = 1/expr.denominator - coeff, new_int_g = _merge_int_g_expr(expr.numerator, replacements) + coeff, new_int_g = _merge_int_g_expr(expr.numerator) new_densities = (density * mult for density in new_int_g.densities) return coeff * mult, new_int_g.copy(densities=new_densities) else: From 5fef79e2e0ff796dfa730293a9091b7e98c89c66 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 14:38:32 -0500 Subject: [PATCH 093/209] use ValueError instead of asserts --- pytential/symbolic/pde/system_utils.py | 30 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 43a5e0ee5..7306dc573 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -90,6 +90,9 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, [get_int_g_mapper(expr) for expr in exprs] int_g_s = mapper.int_g_s replacements = {} + + # Iterate all the IntGs in the expressions and create a dictionary + # of replacements for the IntGs for int_g in int_g_s: # First convert IntGs with target derivatives to source derivatives new_int_g = _convert_target_deriv_to_source(int_g) @@ -102,6 +105,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, replacements[int_g] = sum(convert_int_g_to_base(new_int_g, base_kernel, verbose=verbose) for new_int_g in new_int_g_s) + # replace the IntGs in the expressions substitutor = IntGSubstitutor(replacements) exprs = [subsitutor(expr) for expr in exprs] @@ -113,8 +117,10 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, result_coeffs.append(expr) result_int_gs.append(0) try: - # run the main routine to merge IntG expressions with + # run the main routine to merge IntG expressions result_coeff, result_int_g = _merge_int_g_expr(expr) + # replace an IntG with d axis source derivatives to an IntG + # with one directional source derivative result_int_g = _convert_axis_source_to_directional_source(result_int_g) # simplify the densities as they may become large due to pymbolic # not doing automatic simplifications unlike sympy/symengine @@ -122,7 +128,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, densities=_simplify_densities(result_int_g.densities)) result_coeffs.append(result_coeff) result_int_gs.append(result_int_g) - except AssertionError: + except ValueError: result_coeffs.append(expr) result_int_gs.append(0) @@ -300,7 +306,8 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): if tgt_knl != tgt_knl.get_base_kernel(): return int_g - assert len(int_g.densities) == 1 + if not len(int_g.densities) == 1: + raise ValueError density = int_g.densities[0] source_kernel = int_g.source_kernels[0] @@ -485,7 +492,8 @@ def _merge_kernel_arguments(x, y): res = x.copy() for k, v in y.items(): if k in res: - assert res[k] == v + if not res[k] == v: + raise ValueError else: res[k] = v return res @@ -517,17 +525,17 @@ def _merge_int_g_expr(expr): if result_int_g == 0: result_int_g = int_g continue - assert result_int_g.source == int_g.source - assert result_int_g.target == int_g.target - assert result_int_g.qbx_forced_limit == int_g.qbx_forced_limit - assert result_int_g.target_kernel == int_g.target_kernel + if (result_int_g.source != int_g.source or + result_int_g.target != int_g.target or + result_int_g.qbx_forced_limit != int_g.qbx_forced_limit or + result_int_g.target_kernel != int_g.target_kernel): + raise ValueError + kernel_arguments = _merge_kernel_arguments(result_int_g.kernel_arguments, int_g.kernel_arguments) source_kernels = result_int_g.source_kernels + int_g.source_kernels densities = result_int_g.densities + int_g.densities new_source_kernels, new_densities = source_kernels, densities - # new_source_kernels, new_densities = \ - # _merge_source_kernel_duplicates(source_kernels, densities) result_int_g = result_int_g.copy( source_kernels=tuple(new_source_kernels), densities=tuple(new_densities), @@ -541,7 +549,7 @@ def _merge_int_g_expr(expr): if not have_int_g_s(c): mult *= c elif found_int_g: - raise RuntimeError("Not a linear expression.") + raise ValueError("Not a linear expression.") else: found_int_g = c if not found_int_g: From c695e0295401a1793aeef8d7c9bf36401c773fb2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 14:44:33 -0500 Subject: [PATCH 094/209] fix typos --- pytential/symbolic/pde/system_utils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 7306dc573..9249c0a92 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -88,7 +88,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, if base_kernel is not None: get_int_g_mapper = GetIntGs() [get_int_g_mapper(expr) for expr in exprs] - int_g_s = mapper.int_g_s + int_g_s = get_int_g_mapper.int_g_s replacements = {} # Iterate all the IntGs in the expressions and create a dictionary @@ -96,7 +96,6 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, for int_g in int_g_s: # First convert IntGs with target derivatives to source derivatives new_int_g = _convert_target_deriv_to_source(int_g) - tgt_knl = new_int_g.target_kernel # Convert IntGs with TargetMultiplier to a sum of IntGs without # TargetMultipliers new_int_g_s = _convert_target_multiplier_to_source(new_int_g) @@ -107,7 +106,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # replace the IntGs in the expressions substitutor = IntGSubstitutor(replacements) - exprs = [subsitutor(expr) for expr in exprs] + exprs = [substitutor(expr) for expr in exprs] result_coeffs = [] result_int_gs = [] @@ -525,10 +524,10 @@ def _merge_int_g_expr(expr): if result_int_g == 0: result_int_g = int_g continue - if (result_int_g.source != int_g.source or - result_int_g.target != int_g.target or - result_int_g.qbx_forced_limit != int_g.qbx_forced_limit or - result_int_g.target_kernel != int_g.target_kernel): + if (result_int_g.source != int_g.source + or result_int_g.target != int_g.target + or result_int_g.qbx_forced_limit != int_g.qbx_forced_limit + or result_int_g.target_kernel != int_g.target_kernel): raise ValueError kernel_arguments = _merge_kernel_arguments(result_int_g.kernel_arguments, From 30697eb26f4c3de2dc5b69b6aa78f0276753f846 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 15:06:15 -0500 Subject: [PATCH 095/209] docs for evalf --- pytential/symbolic/pde/system_utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 9249c0a92..8f840fcaa 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -149,14 +149,17 @@ def map_int_g(self, expr): return self.replacements.get(expr, expr) -def evalf(expr): +def evalf(expr, prec=100): + """evaluate an expression numerically using ``prec`` + number of bits. + """ from sumpy.symbolic import USE_SYMENGINE if USE_SYMENGINE: - # 100 bits - return expr.n(prec=100) + return expr.n(prec=prec) else: - # 30 decimal places - return expr.n(n=30) + import sympy + dps = int(sympy.log(2**prec, 10)) + return expr.n(n=dps) @memoize_on_first_arg From eeadafd647e60a22ff3a1ea96d7dcbc1225bba97 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 15:57:30 -0500 Subject: [PATCH 096/209] more docs --- pytential/symbolic/pde/system_utils.py | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 8f840fcaa..49ef4a42b 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -95,10 +95,10 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # of replacements for the IntGs for int_g in int_g_s: # First convert IntGs with target derivatives to source derivatives - new_int_g = _convert_target_deriv_to_source(int_g) + new_int_g = convert_target_deriv_to_source(int_g) # Convert IntGs with TargetMultiplier to a sum of IntGs without # TargetMultipliers - new_int_g_s = _convert_target_multiplier_to_source(new_int_g) + new_int_g_s = convert_target_multiplier_to_source(new_int_g) # Convert IntGs with different kernels to expressions containing # IntGs with base_kernel or its derivatives replacements[int_g] = sum(convert_int_g_to_base(new_int_g, @@ -124,7 +124,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # simplify the densities as they may become large due to pymbolic # not doing automatic simplifications unlike sympy/symengine result_int_g = result_int_g.copy( - densities=_simplify_densities(result_int_g.densities)) + densities=simplify_densities(result_int_g.densities)) result_coeffs.append(result_coeff) result_int_gs.append(result_int_g) except ValueError: @@ -386,7 +386,10 @@ def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): return result -def _convert_target_multiplier_to_source(int_g): +def convert_target_multiplier_to_source(int_g): + """Convert an IntG with TargetMultiplier to an sum of IntGs without + TargetMultiplier and only source dependent transformations + """ from sumpy.symbolic import SympyToPymbolicMapper tgt_knl = int_g.target_kernel if not isinstance(tgt_knl, TargetPointMultiplier): @@ -396,6 +399,7 @@ def _convert_target_multiplier_to_source(int_g): new_kernel_args = _filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) result = [] + # If the kernel is G, source is y and target is x, # x G = y*G + (x - y)*G # For y*G, absorb y into a density new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) @@ -419,7 +423,7 @@ def _convert_target_multiplier_to_source(int_g): return result -def _convert_target_deriv_to_source(int_g): +def convert_target_deriv_to_source(int_g): knl = int_g.target_kernel source_kernels = list(int_g.source_kernels) coeff = 1 @@ -490,7 +494,10 @@ def _merge_source_kernel_duplicates(source_kernels, densities): return new_source_kernels, new_densities -def _merge_kernel_arguments(x, y): +def merge_kernel_arguments(x, y): + """merge two kernel argument dictionaries and raise a ValueError if + the two dictionaries do not agree for duplicate keys. + """ res = x.copy() for k, v in y.items(): if k in res: @@ -501,7 +508,10 @@ def _merge_kernel_arguments(x, y): return res -def _simplify_densities(densities): +def simplify_densities(densities): + """Simplify densities by converting to sympy and converting back + to trigger sympy's automatic simplification routines. + """ from sumpy.symbolic import (SympyToPymbolicMapper, PymbolicToSympyMapper) from pymbolic.mapper import UnsupportedExpressionError to_sympy = PymbolicToSympyMapper() @@ -533,7 +543,7 @@ def _merge_int_g_expr(expr): or result_int_g.target_kernel != int_g.target_kernel): raise ValueError - kernel_arguments = _merge_kernel_arguments(result_int_g.kernel_arguments, + kernel_arguments = merge_kernel_arguments(result_int_g.kernel_arguments, int_g.kernel_arguments) source_kernels = result_int_g.source_kernels + int_g.source_kernels densities = result_int_g.densities + int_g.densities @@ -561,7 +571,7 @@ def _merge_int_g_expr(expr): new_densities = (density * mult for density in new_int_g.densities) return coeff*mult, new_int_g.copy(densities=new_densities) elif isinstance(expr, IntG): - new_int_g = _convert_target_deriv_to_source(expr) + new_int_g = convert_target_deriv_to_source(expr) return 0, new_int_g elif isinstance(expr, Quotient): mult = 1/expr.denominator From c794ad04decd1256f08aabf108e5e293d4c323e2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 2 Sep 2021 15:59:06 -0500 Subject: [PATCH 097/209] Fix return of IntGs --- pytential/symbolic/pde/system_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 49ef4a42b..f4b8f91dd 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -393,9 +393,9 @@ def convert_target_multiplier_to_source(int_g): from sumpy.symbolic import SympyToPymbolicMapper tgt_knl = int_g.target_kernel if not isinstance(tgt_knl, TargetPointMultiplier): - return int_g + return [int_g] if isinstance(tgt_knl.inner_kernel, KernelWrapper): - return int_g + return [int_g] new_kernel_args = _filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) result = [] From 78d9234ffb2e0dddfa95da61d9b18a7a6b26c62f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 7 Sep 2021 21:22:49 -0500 Subject: [PATCH 098/209] use a mapper instead of _merge_int_g_expr --- pytential/symbolic/pde/system_utils.py | 101 +++++++++++++++---------- 1 file changed, 59 insertions(+), 42 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index f4b8f91dd..c20137f83 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -30,14 +30,14 @@ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) -from pymbolic.mapper import WalkMapper +from pymbolic.mapper import WalkMapper, Mapper from pymbolic.primitives import Sum, Product, Quotient from pytential.symbolic.primitives import IntG, NodeCoordinateComponent from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand import pytential -from .reduce_fmms import reduce_number_of_fmms +from pytential.symbolic.pde.reduce_fmms import reduce_number_of_fmms __all__ = ( "merge_int_g_exprs", @@ -111,16 +111,17 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, result_coeffs = [] result_int_gs = [] + merge_int_g_expr_mapper = MergeIntGExpr() for expr in exprs: if not have_int_g_s(expr): result_coeffs.append(expr) result_int_gs.append(0) try: # run the main routine to merge IntG expressions - result_coeff, result_int_g = _merge_int_g_expr(expr) + result_coeff, result_int_g = merge_int_g_expr_mapper(expr) # replace an IntG with d axis source derivatives to an IntG # with one directional source derivative - result_int_g = _convert_axis_source_to_directional_source(result_int_g) + result_int_g = convert_axis_source_to_directional_source(result_int_g) # simplify the densities as they may become large due to pymbolic # not doing automatic simplifications unlike sympy/symengine result_int_g = result_int_g.copy( @@ -283,16 +284,11 @@ def have_int_g_s(expr): return bool(mapper.int_g_s) -def convert_int_g_to_base(int_g, base_kernel, verbose=False): - result = 0 - for knl, density in zip(int_g.source_kernels, int_g.densities): - result += _convert_int_g_to_base( - int_g.copy(source_kernels=(knl,), densities=(density,)), - base_kernel, verbose) - return result - - -def _filter_kernel_arguments(knls, kernel_arguments): +def filter_kernel_arguments(knls, kernel_arguments): + """From a dictionary of kernel arguments, filter out arguments + that are not needed for the kernels given as a list and return a new + dictionary. + """ kernel_arg_names = set() for kernel in knls: @@ -302,6 +298,15 @@ def _filter_kernel_arguments(knls, kernel_arguments): return {k: v for (k, v) in kernel_arguments.items() if k in kernel_arg_names} +def convert_int_g_to_base(int_g, base_kernel, verbose=False): + result = 0 + for knl, density in zip(int_g.source_kernels, int_g.densities): + result += _convert_int_g_to_base( + int_g.copy(source_kernels=(knl,), densities=(density,)), + base_kernel, verbose) + return result + + def _convert_int_g_to_base(int_g, base_kernel, verbose=False): tgt_knl = int_g.target_kernel dim = tgt_knl.dim @@ -328,7 +333,7 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): if source_kernel == source_kernel.get_base_kernel(): result += const - new_kernel_args = _filter_kernel_arguments([base_kernel], int_g.kernel_arguments) + new_kernel_args = filter_kernel_arguments([base_kernel], int_g.kernel_arguments) for mi, c in deriv_relation[1]: knl = source_kernel.replace_base_kernel(base_kernel) @@ -397,7 +402,7 @@ def convert_target_multiplier_to_source(int_g): if isinstance(tgt_knl.inner_kernel, KernelWrapper): return [int_g] - new_kernel_args = _filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) + new_kernel_args = filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) result = [] # If the kernel is G, source is y and target is x, # x G = y*G + (x - y)*G @@ -424,6 +429,10 @@ def convert_target_multiplier_to_source(int_g): def convert_target_deriv_to_source(int_g): + """Converts AxisTargetDerivatives to AxisSourceDerivative instances + from an IntG. If there are outer TargetPointMultiplier transformations + they are preserved. + """ knl = int_g.target_kernel source_kernels = list(int_g.source_kernels) coeff = 1 @@ -452,7 +461,10 @@ def convert_target_deriv_to_source(int_g): source_kernels=tuple(source_kernels)) -def _convert_axis_source_to_directional_source(int_g): +def convert_axis_source_to_directional_source(int_g): + """Convert an IntG with d AxisSourceDerivative instances to + an IntG with one DirectionalSourceDerivative instance. + """ if not isinstance(int_g, IntG): return int_g knls = list(int_g.source_kernels) @@ -481,19 +493,6 @@ def _convert_axis_source_to_directional_source(int_g): return res -def _merge_source_kernel_duplicates(source_kernels, densities): - new_source_kernels = [] - new_densities = [] - for knl, density in zip(source_kernels, densities): - if knl not in new_source_kernels: - new_source_kernels.append(knl) - new_densities.append(density) - else: - idx = new_source_kernels.index(knl) - new_densities[idx] += density - return new_source_kernels, new_densities - - def merge_kernel_arguments(x, y): """merge two kernel argument dictionaries and raise a ValueError if the two dictionaries do not agree for duplicate keys. @@ -525,18 +524,23 @@ def simplify_densities(densities): return tuple(result) -def _merge_int_g_expr(expr): - if isinstance(expr, Sum): +class MergeIntGExpr(Mapper): + """Given an expression, return a tuple of (constant, IntG) where + the constant does not have any IntG expressions in them. + """ + def map_sum(self, expr): result_coeff = 0 result_int_g = 0 for c in expr.children: - coeff, int_g = _merge_int_g_expr(c) + coeff, int_g = self.rec(c) result_coeff += coeff if int_g == 0: continue if result_int_g == 0: + # This is the first IntG we have encountered result_int_g = int_g continue + # Check that the two IntGs are compatible if (result_int_g.source != int_g.source or result_int_g.target != int_g.target or result_int_g.qbx_forced_limit != int_g.qbx_forced_limit @@ -554,7 +558,8 @@ def _merge_int_g_expr(expr): kernel_arguments=kernel_arguments, ) return result_coeff, result_int_g - elif isinstance(expr, Product): + + def map_product(self, expr): mult = 1 found_int_g = None for c in expr.children: @@ -567,20 +572,31 @@ def _merge_int_g_expr(expr): if not found_int_g: return expr, 0 else: - coeff, new_int_g = _merge_int_g_expr(found_int_g) + coeff, new_int_g = self.rec(found_int_g) new_densities = (density * mult for density in new_int_g.densities) return coeff*mult, new_int_g.copy(densities=new_densities) - elif isinstance(expr, IntG): + + def map_int_g(self, expr): new_int_g = convert_target_deriv_to_source(expr) return 0, new_int_g - elif isinstance(expr, Quotient): - mult = 1/expr.denominator - coeff, new_int_g = _merge_int_g_expr(expr.numerator) + + def map_quotient(self, expr): + mult = Quotient(1, expr.denominator) + coeff, new_int_g = self.rec(expr.numerator) new_densities = (density * mult for density in new_int_g.densities) return coeff * mult, new_int_g.copy(densities=new_densities) - else: + + def handle_unsupported_expression(self, expr, *args, **kwargs): return expr, 0 + def __call__(self, expr): + try: + return super().__call__(expr) + except NotImplementedError: + return expr, 0 + + rec = __call__ + if __name__ == "__main__": from sumpy.kernel import (StokesletKernel, BiharmonicKernel, # noqa:F401 @@ -601,7 +617,8 @@ def _merge_int_g_expr(expr): #kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), # ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=True) - """density = pytential.sym.make_sym_vector("d", 1)[0] + density = pytential.sym.make_sym_vector("d", 1)[0] + from pytential.symbolic.primitives import int_g_vec int_g_1 = int_g_vec(TargetPointMultiplier(2, AxisTargetDerivative(2, AxisSourceDerivative(1, AxisSourceDerivative(0, LaplaceKernel(3))))), density, qbx_forced_limit=1) @@ -609,4 +626,4 @@ def _merge_int_g_expr(expr): AxisSourceDerivative(0, AxisSourceDerivative(0, LaplaceKernel(3))))), density, qbx_forced_limit=1) print(merge_int_g_exprs([int_g_1, int_g_2], - base_kernel=BiharmonicKernel(3), verbose=True)[0])""" + base_kernel=BiharmonicKernel(3), verbose=True)[0]) From 37a7941a23f2f5d25a007fa120d69e77137bc6de Mon Sep 17 00:00:00 2001 From: Alex Fikl Date: Tue, 7 Sep 2021 21:28:51 -0500 Subject: [PATCH 099/209] Fix extra_deriv_dirs for tornberg --- pytential/symbolic/stokes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index c376e4295..7d00cefa3 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -524,7 +524,12 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, qbx_forced_limit=qbx_forced_limit) if i == j: - sym_expr[i] += sym.IntG(target_kernel=self.kernel, + target_kernel = self.kernel + for deriv_dir in extra_deriv_dirs: + target_kernel = AxisTargetDerivative( + deriv_dir, target_kernel) + + sym_expr[i] += sym.IntG(target_kernel=target_kernel, source_kernels=common_source_kernels, densities=densities, qbx_forced_limit=qbx_forced_limit) From d2d9348a62e546ebada6e75212f40cead6cacccd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 01:18:32 -0500 Subject: [PATCH 100/209] remove unused imports --- pytential/symbolic/pde/system_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index c20137f83..9fb34f243 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -31,7 +31,7 @@ as gnitstam) from pymbolic.mapper import WalkMapper, Mapper -from pymbolic.primitives import Sum, Product, Quotient +from pymbolic.primitives import Quotient from pytential.symbolic.primitives import IntG, NodeCoordinateComponent from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand From 5ea14e2cf8e4f43b7e254fe7f8dd8db9f6bc2092 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 02:44:04 -0500 Subject: [PATCH 101/209] fix when syzygy module can't be computed --- pytential/symbolic/pde/reduce_fmms.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 3a37a1b93..51950c772 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -75,8 +75,11 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): mat = sympy.Matrix(mat) # Factor the matrix into two - lhs_mat = _factor_left(mat, axis_vars) - rhs_mat = _factor_right(mat, lhs_mat) + try: + lhs_mat = _factor_left(mat, axis_vars) + rhs_mat = _factor_right(mat, lhs_mat) + except ValueError: + return int_gs # If there are n inputs and m outputs, # @@ -202,6 +205,7 @@ def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): operator. First it is converted to a :class:`sumpy.kernel.Kernel` and then to a :class:`~pytential.symbolic.primitives.IntG`. """ + from pytential.symbolic.pde.system_utils import simplify_densities to_pymbolic = SympyToPymbolicMapper() orig_kernel = orig_int_g.source_kernels[0] @@ -215,7 +219,7 @@ def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): source_kernels.append(kernel) densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) return orig_int_g.copy(source_kernels=tuple(source_kernels), - densities=tuple(densities)) + densities=tuple(simplify_densities(densities))) def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): @@ -252,6 +256,12 @@ def __call__(self, expr): return {expr: 1} return super().__call__(expr) + def map_algebraic_leaf(self, expr): + if expr in self.source_dependent_variables: + return {expr: 1} + else: + return {1: expr} + rec = __call__ @@ -277,6 +287,9 @@ def _convert_to_matrix(module, *gens): for i in range(m.shape[1])] column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] + if not column_syzygy_modules: + raise ValueError + intersection = column_syzygy_modules[0] for i in range(1, len(column_syzygy_modules)): intersection = intersection.intersect(column_syzygy_modules[i]) From afe3be807c33930500040dd50747a5bdf495eb7f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 02:45:39 -0500 Subject: [PATCH 102/209] Add some tests for system_utils --- test/test_pde_system_utils.py | 163 ++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 test/test_pde_system_utils.py diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py new file mode 100644 index 000000000..1403a6560 --- /dev/null +++ b/test/test_pde_system_utils.py @@ -0,0 +1,163 @@ +__copyright__ = "Copyright (C) 2021 Isuru Fernando" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from sumpy.kernel import (LaplaceKernel, AxisSourceDerivative, + AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel) +from pytential.symbolic.primitives import int_g_vec, IntG, NodeCoordinateComponent +from pytential.symbolic.pde.system_utils import merge_int_g_exprs +from pymbolic.primitives import make_sym_vector, Variable + + +def test_reduce_number_of_fmms(): + dim = 3 + knl = LaplaceKernel(dim) + densities = make_sym_vector("sigma", 2) + mu = Variable("mu") + + int_g1 = \ + int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), + densities[0] * mu, qbx_forced_limit=1) + \ + int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(1, knl)), + densities[1] * mu, qbx_forced_limit=1) + + int_g2 = \ + int_g_vec(AxisSourceDerivative(2, AxisSourceDerivative(0, knl)), + densities[0], qbx_forced_limit=1) + \ + int_g_vec(AxisSourceDerivative(2, AxisSourceDerivative(1, knl)), + densities[1], qbx_forced_limit=1) + + # Merging reduces 4 FMMs to 2 FMMs and then further reduced to 1 FMM + result = merge_int_g_exprs([int_g1, int_g2], source_dependent_variables=[]) + + int_g3 = \ + IntG(target_kernel=AxisTargetDerivative(1, knl), + source_kernels=[AxisSourceDerivative(0, knl), + AxisSourceDerivative(1, knl)], + densities=[-mu * densities[0], -mu * densities[1]], + qbx_forced_limit=1) + + int_g4 = \ + IntG(target_kernel=AxisTargetDerivative(2, knl), + source_kernels=[AxisSourceDerivative(0, knl), + AxisSourceDerivative(1, knl)], + densities=[-mu * densities[0], -mu * densities[1]], + qbx_forced_limit=1) + + assert result[0] == int_g3 + assert result[1] == int_g4 * mu**(-1) + + +def test_source_dependent_variable(): + # Same example as test_reduce_number_of_fmms, but with + # mu marked as a source dependent variable + dim = 3 + knl = LaplaceKernel(dim) + densities = make_sym_vector("sigma", 2) + mu = Variable("mu") + + int_g1 = \ + int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), + mu * densities[0], qbx_forced_limit=1) + \ + int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(1, knl)), + mu * densities[1], qbx_forced_limit=1) + + int_g2 = \ + int_g_vec(AxisSourceDerivative(2, AxisSourceDerivative(0, knl)), + densities[0], qbx_forced_limit=1) + \ + int_g_vec(AxisSourceDerivative(2, AxisSourceDerivative(1, knl)), + densities[1], qbx_forced_limit=1) + + result = merge_int_g_exprs([int_g1, int_g2], + source_dependent_variables=[mu]) + + # Merging reduces 4 FMMs to 2 FMMs. No further reduction of FMMs. + int_g3 = \ + IntG(target_kernel=knl, + source_kernels=[AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), + AxisSourceDerivative(1, AxisSourceDerivative(1, knl))], + densities=[mu * densities[0], mu * densities[1]], + qbx_forced_limit=1) + + int_g4 = \ + IntG(target_kernel=knl, + source_kernels=[AxisSourceDerivative(2, AxisSourceDerivative(0, knl)), + AxisSourceDerivative(2, AxisSourceDerivative(1, knl))], + densities=[densities[0], densities[1]], + qbx_forced_limit=1) + + assert result[0] == int_g3 + assert result[1] == int_g4 + + +def test_base_kernel_merge(): + # Same example as test_reduce_number_of_fmms, but with + # mu marked as a source dependent variable + dim = 3 + knl = LaplaceKernel(dim) + biharm_knl = BiharmonicKernel(dim) + density = make_sym_vector("sigma", 1)[0] + + int_g1 = \ + int_g_vec(TargetPointMultiplier(0, knl), + density, qbx_forced_limit=1) + + int_g2 = \ + int_g_vec(TargetPointMultiplier(1, knl), + density, qbx_forced_limit=1) + + result = merge_int_g_exprs([int_g1, int_g2], + source_dependent_variables=[], + base_kernel=biharm_knl) + + sources = [NodeCoordinateComponent(i) for i in range(dim)] + + source_kernels = [ + AxisSourceDerivative(i, AxisSourceDerivative(i, biharm_knl)) + for i in range(dim)] + + int_g3 = IntG(target_kernel=biharm_knl, + source_kernels=source_kernels + [AxisSourceDerivative(0, biharm_knl)], + densities=[density*sources[0]*(-1.0) for i in range(dim)] + + [2*density], + qbx_forced_limit=1) + int_g4 = IntG(target_kernel=biharm_knl, + source_kernels=source_kernels + [AxisSourceDerivative(1, biharm_knl)], + densities=[density*sources[1]*(-1.0) for i in range(dim)] + + [2*density], + qbx_forced_limit=1) + + assert int_g3 == result[0] + assert int_g4 == result[1] + + +# You can test individual routines by typing +# $ python test_pde_system_tools.py 'test_reduce_number_of_fmms()' + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1: + exec(sys.argv[1]) + else: + from pytest import main + main([__file__]) + +# vim: fdm=marker From 566ae5b9ab4f22ab4c8e3a3f4406d7cdd8e90842 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 10:09:51 -0500 Subject: [PATCH 103/209] Fix f-strings. (No need of $) --- experiments/stokes-2d-interior.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/experiments/stokes-2d-interior.py b/experiments/stokes-2d-interior.py index 5a9ce1577..57727d273 100644 --- a/experiments/stokes-2d-interior.py +++ b/experiments/stokes-2d-interior.py @@ -200,7 +200,7 @@ def stride_hack(arr): pressure = bind((qbx, PointsTarget(eval_points_dev)), rep_pressure)(queue, sigma=sigma, mu=mu, normal=normal) pressure = pressure.get() - print(f"pressure = ${pressure}") + print(f"pressure = {pressure}") x_dir_vecs = np.zeros((2,len(eval_points[0]))) x_dir_vecs[0,:] = 1.0 @@ -218,8 +218,8 @@ def stride_hack(arr): rep_stress)(queue, sigma=sigma, normal=normal, force_direction=y_dir_vecs, mu=mu) applied_stress_y = get_obj_array(applied_stress_y) - print(f"stress applied to x direction: ${applied_stress_x}") - print(f"stress applied to y direction: ${applied_stress_y}") + print(f"stress applied to x direction: {applied_stress_x}") + print(f"stress applied to y direction: {applied_stress_y}") import matplotlib.pyplot as plt From e441b522249469e89d826a8d3bfdcc431bbb4bae Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 14:47:14 -0500 Subject: [PATCH 104/209] Improve docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- pytential/symbolic/pde/reduce_fmms.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 51950c772..41aef66cd 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -39,13 +39,13 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): :class:`~pytential.symbolic.primitives.IntG` objects. This is done by converting the ``IntG`` object to a matrix of polynomials - with d variables corresponding to d dimensions and each polynomial represents - a derivative operator. All the properties of derivative operator that we want + with d variables corresponding to d dimensions, where each variable represents + a (target) derivative operator along one of the axes. All the properties of derivative operator that we want are reflected in the properties of the polynomial including addition, multiplication and exact polynomial division. - This matrix is factored into two matrices where the left hand side matrix - represents a transformation at the target and the right hand side matrix + This matrix is factored into two matrices, where the left hand side matrix + represents a transformation at the target, and the right hand side matrix represents a transformation at the source. If the expressions given are not linear, then the input expressions are @@ -57,8 +57,10 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): objects. When reducing FMMs, consider only these variables as dependent on source. For eg: densities, source derivative vectors. - Note: there is no argument for target dependent variables as the algorithm - assumes that there are no target dependent variables passed to this function. + Note: there is no argument for target-dependent variables as the algorithm + assumes that there are no target-dependent-variables passed to this function. + (where a "target-dependent variable" represents a function discretized on the + targets) """ dim = int_gs[0].target_kernel.dim From 053c08582a0dd8aa8b9ddbd9252258371f93d968 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 14:46:33 -0500 Subject: [PATCH 105/209] gens -> generators --- pytential/symbolic/pde/reduce_fmms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 41aef66cd..645957a0b 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -267,24 +267,24 @@ def map_algebraic_leaf(self, expr): rec = __call__ -def _syzygy_module(m, gens): +def _syzygy_module(m, generators): """Takes as input a module of polynomials with domain `sympy.EX` represented as a matrix and returns the syzygy module as a matrix of polynomials in the same domain. """ from sympy.polys.orderings import grevlex - def _convert_to_matrix(module, *gens): + def _convert_to_matrix(module, *generators): result = [] for syzygy in module: row = [] for dmp in syzygy.data: - row.append(sympy.Poly(dmp.to_dict(), *gens, + row.append(sympy.Poly(dmp.to_dict(), *generators, domain=sympy.EX).as_expr()) result.append(row) return sympy.Matrix(result) - ring = sympy.EX.old_poly_ring(*gens, order=grevlex) + ring = sympy.EX.old_poly_ring(*generators, order=grevlex) column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist(), order=grevlex) for i in range(m.shape[1])] column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] @@ -297,7 +297,7 @@ def _convert_to_matrix(module, *gens): intersection = intersection.intersect(column_syzygy_modules[i]) m2 = intersection._groebner_vec() - m3 = _convert_to_matrix(m2, *gens) + m3 = _convert_to_matrix(m2, *generators) return m3 From 801e5fbdbd3e467d422fc9cd04907747bc074de6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 14:48:43 -0500 Subject: [PATCH 106/209] _convert_kernel_to_poly -> _kernel_target_derivs_as_poly --- pytential/symbolic/pde/reduce_fmms.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 645957a0b..1e80ee33e 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -152,7 +152,7 @@ def _create_matrix(int_gs, source_dependent_variables, axis_vars): if source_expr not in source_exprs: source_exprs.append(source_expr) row += [0] - poly = _convert_kernel_to_poly(source_kernel, axis_vars) + poly = _kernel_target_derivs_as_poly(source_kernel, axis_vars) row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) matrix.append(row) @@ -183,7 +183,7 @@ def _check_int_gs_common(int_gs): return True -def _convert_kernel_to_poly(kernel, axis_vars): +def _kernel_target_derivs_as_poly(kernel, axis_vars): """Converts a :class:`sumpy.kernel.Kernel` object to a polynomial. A :class:`sumpy.kernel.Kernel` represents a derivative operator and the derivative operator is converted to a polynomial with @@ -193,16 +193,16 @@ def _convert_kernel_to_poly(kernel, axis_vars): d/dx_1 dy_2 + d/dy_1 is converted to -y_2 * y_1 + y_1. """ if isinstance(kernel, AxisTargetDerivative): - poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) + poly = _kernel_target_derivs_as_poly(kernel.inner_kernel, axis_vars) return axis_vars[kernel.axis]*poly elif isinstance(kernel, AxisSourceDerivative): - poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) + poly = _kernel_target_derivs_as_poly(kernel.inner_kernel, axis_vars) return -axis_vars[kernel.axis]*poly return 1 def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): - """This does the opposite of :func:`_convert_kernel_to_poly` + """This does the opposite of :func:`_kernel_target_derivs_as_poly` and converts a polynomial back to a source derivative operator. First it is converted to a :class:`sumpy.kernel.Kernel` and then to a :class:`~pytential.symbolic.primitives.IntG`. @@ -225,7 +225,7 @@ def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): - """This does the opposite of :func:`_convert_kernel_to_poly` + """This does the opposite of :func:`_kernel_target_derivs_as_poly` and converts a polynomial back to a target derivative operator. It is applied to a :class:`~pytential.symbolic.primitives.IntG` object and returns a new instance. From e7a4afb9e4f5660c22df16014e7be940cca17d42 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 14:51:31 -0500 Subject: [PATCH 107/209] Use functools.reduce --- pytential/symbolic/pde/reduce_fmms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 1e80ee33e..13ad5cd3c 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -26,6 +26,7 @@ from pymbolic.mapper.coefficient import ( CoefficientCollector as CoefficientCollectorBase) import sympy +import functools __all__ = ( @@ -292,9 +293,7 @@ def _convert_to_matrix(module, *generators): if not column_syzygy_modules: raise ValueError - intersection = column_syzygy_modules[0] - for i in range(1, len(column_syzygy_modules)): - intersection = intersection.intersect(column_syzygy_modules[i]) + functools.reduce(lambda x, y: x.intersect(y), column_syzygy_modules) m2 = intersection._groebner_vec() m3 = _convert_to_matrix(m2, *generators) From 93aad17e67f10d52134111d21f64cced2758d515 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 16:11:44 -0500 Subject: [PATCH 108/209] Fix coefficient collector --- pytential/symbolic/pde/reduce_fmms.py | 74 ++++++++++++++++++++++++--- test/test_pde_system_utils.py | 9 ++-- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 13ad5cd3c..6050c49ce 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -23,10 +23,11 @@ from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative) from pymbolic.interop.sympy import PymbolicToSympyMapper, SympyToPymbolicMapper -from pymbolic.mapper.coefficient import ( - CoefficientCollector as CoefficientCollectorBase) +from pymbolic.mapper import Mapper +from pymbolic.primitives import Product import sympy import functools +from collections import defaultdict __all__ = ( @@ -246,10 +247,12 @@ def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): return result -class CoefficientCollector(CoefficientCollectorBase): - """This extends :class:`pymbolic.mapper.coefficient.CoefficientCollector` - by extending the targets to be any expression instead of just algebraic - leafs +class CoefficientCollector(Mapper): + """From a density expression, extracts expressions that need to be + evaluated for each source and coefficients for each expression. + + For eg: when this mapper is given as ``s*(s + 2) + 3`` input, + it returns {s**2: 1, s: 2, 1: 3}. """ def __init__(self, source_dependent_variables): self.source_dependent_variables = source_dependent_variables @@ -259,12 +262,69 @@ def __call__(self, expr): return {expr: 1} return super().__call__(expr) + def map_sum(self, expr): + stride_dicts = [self.rec(ch) for ch in expr.children] + + result = defaultdict(lambda: 0) + for stride_dict in stride_dicts: + for var, stride in stride_dict.items(): + result[var] += stride + return dict(result) + def map_algebraic_leaf(self, expr): if expr in self.source_dependent_variables: return {expr: 1} else: return {1: expr} + def map_constant(self, expr): + return {1: expr} + + def map_product(self, expr): + if len(expr.children) > 2: + left = Product(tuple(expr.children[:2])) + right = Product(tuple(expr.children[2:])) + new_prod = Product((left, right)) + return self.rec(new_prod) + elif len(expr.children) == 1: + return self.rec(expr.children[0]) + elif len(expr.children) == 0: + return {1: 1} + left, right = expr.children + d_left = self.rec(left) + d_right = self.rec(right) + d = defaultdict(lambda: 0) + for k_left, v_left in d_left.items(): + for k_right, v_right in d_right.items(): + d[k_left*k_right] += v_left*v_right + return dict(d) + + def map_quotient(self, expr): + d_num = self.rec(expr.numerator) + d_den = self.rec(expr.denominator) + if len(d_den) > 1: + raise ValueError + den_k, den_v = list(d_den.items())[0] + + result = {} + for k in d_num.keys(): + result[d_num[k]/den_k] /= den_v + return result + + def map_power(self, expr): + d_base = self.rec(expr.base) + d_exponent = self.rec(expr.exponent) + # d_exponent should look like {1: k} + if len(d_exponent) > 1 or 1 not in d_exponent: + raise RuntimeError("nonlinear expression") + exp = list(d_exponent.values())[0] + if exp == 1: + return d_base + if len(d_base) > 1: + raise NotImplementedError("powers are not implemented") + (k, v), = d_base.items() + return {k**exp: v**exp} + rec = __call__ @@ -293,7 +353,7 @@ def _convert_to_matrix(module, *generators): if not column_syzygy_modules: raise ValueError - functools.reduce(lambda x, y: x.intersect(y), column_syzygy_modules) + intersection = functools.reduce(lambda x, y: x.intersect(y), column_syzygy_modules) m2 = intersection._groebner_vec() m3 = _convert_to_matrix(m2, *generators) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 1403a6560..0c5d899a7 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -73,12 +73,13 @@ def test_source_dependent_variable(): knl = LaplaceKernel(dim) densities = make_sym_vector("sigma", 2) mu = Variable("mu") + nu = Variable("nu") int_g1 = \ int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), - mu * densities[0], qbx_forced_limit=1) + \ + mu * nu * densities[0], qbx_forced_limit=1) + \ int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(1, knl)), - mu * densities[1], qbx_forced_limit=1) + mu * nu * densities[1], qbx_forced_limit=1) int_g2 = \ int_g_vec(AxisSourceDerivative(2, AxisSourceDerivative(0, knl)), @@ -87,14 +88,14 @@ def test_source_dependent_variable(): densities[1], qbx_forced_limit=1) result = merge_int_g_exprs([int_g1, int_g2], - source_dependent_variables=[mu]) + source_dependent_variables=[mu, nu]) # Merging reduces 4 FMMs to 2 FMMs. No further reduction of FMMs. int_g3 = \ IntG(target_kernel=knl, source_kernels=[AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), AxisSourceDerivative(1, AxisSourceDerivative(1, knl))], - densities=[mu * densities[0], mu * densities[1]], + densities=[mu * nu * densities[0], mu * nu *densities[1]], qbx_forced_limit=1) int_g4 = \ From bf4adb1be31d4d078b1fedaadc49a1d572be72da Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 16:31:46 -0500 Subject: [PATCH 109/209] describe matrix rows and columns --- pytential/symbolic/pde/reduce_fmms.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 6050c49ce..af77b7ff8 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -136,10 +136,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): def _create_matrix(int_gs, source_dependent_variables, axis_vars): """Create a matrix from a list of :class:`~pytential.symbolic.primitives.IntG` objects and returns the matrix and the expressions corresponding to each column. - Each expression is one of ``source_dependent_variables`` or - equals to one. Each element in the matrix is a multi-variate polynomial - and the variables in the polynomial are from ``axis_vars`` input. - Each polynomial represents a derivative operator. + Each expression is an expression containing ``source_dependent_variables``. + Each element in the matrix is a multi-variate polynomial and the variables + in the polynomial are from ``axis_vars`` input. Each polynomial represents + a derivative operator. + + Number of rows of the returned matrix is equal to the number of ``int_gs`` and + the number of columns is equal to the number of input source dependent + expressions. """ source_exprs = [] coefficient_collector = CoefficientCollector(source_dependent_variables) From b937a9df7a0d7ca231631880031a6141626ce596 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 8 Sep 2021 16:41:27 -0500 Subject: [PATCH 110/209] explain what _groebner_vec returns --- pytential/symbolic/pde/reduce_fmms.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index af77b7ff8..4c17dc84a 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -42,9 +42,9 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): This is done by converting the ``IntG`` object to a matrix of polynomials with d variables corresponding to d dimensions, where each variable represents - a (target) derivative operator along one of the axes. All the properties of derivative operator that we want - are reflected in the properties of the polynomial including addition, - multiplication and exact polynomial division. + a (target) derivative operator along one of the axes. All the properties of + derivative operator that we want are reflected in the properties of the + polynomial including addition, multiplication and exact polynomial division. This matrix is factored into two matrices, where the left hand side matrix represents a transformation at the target, and the right hand side matrix @@ -357,11 +357,12 @@ def _convert_to_matrix(module, *generators): if not column_syzygy_modules: raise ValueError - intersection = functools.reduce(lambda x, y: x.intersect(y), column_syzygy_modules) + intersection = functools.reduce(lambda x, y: x.intersect(y), + column_syzygy_modules) - m2 = intersection._groebner_vec() - m3 = _convert_to_matrix(m2, *generators) - return m3 + # _groebner_vec returns a groebner basis of the syzygy module as a list + groebner_vec = intersection._groebner_vec() + return _convert_to_matrix(groebner_vec, *generators) def _factor_left(mat, axis_vars): From 3e1f76f81bd5da317a17083dc61fc3d82d6e2f43 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 9 Sep 2021 18:50:00 -0500 Subject: [PATCH 111/209] Group IntGs by base_kernel, source, target, qbx_forced_limit --- pytential/symbolic/pde/reduce_fmms.py | 4 +- pytential/symbolic/pde/system_utils.py | 193 ++++++++++++------------- test/test_pde_system_utils.py | 26 ++-- 3 files changed, 108 insertions(+), 115 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 4c17dc84a..2e3a451a1 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -68,8 +68,8 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): dim = int_gs[0].target_kernel.dim axis_vars = sympy.symbols(f"_x0:{dim}") - if not _check_int_gs_common(int_gs): - return int_gs + # A high level driver for this function should send int_gs that are common. + assert _check_int_gs_common(int_gs) try: mat, source_exprs = _create_matrix(int_gs, source_dependent_variables, diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 9fb34f243..377a6d033 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -30,8 +30,8 @@ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) -from pymbolic.mapper import WalkMapper, Mapper -from pymbolic.primitives import Quotient +from pymbolic.mapper import WalkMapper +from pymbolic.mapper.coefficient import CoefficientCollector from pytential.symbolic.primitives import IntG, NodeCoordinateComponent from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand @@ -85,10 +85,9 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, needed for the output. """ + int_g_s = get_int_g_s(exprs) + if base_kernel is not None: - get_int_g_mapper = GetIntGs() - [get_int_g_mapper(expr) for expr in exprs] - int_g_s = get_int_g_mapper.int_g_s replacements = {} # Iterate all the IntGs in the expressions and create a dictionary @@ -108,35 +107,91 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, substitutor = IntGSubstitutor(replacements) exprs = [substitutor(expr) for expr in exprs] - result_coeffs = [] - result_int_gs = [] + groups = [] + exprs_per_groups = [] + + result = np.array([0 for _ in exprs], dtype=object) - merge_int_g_expr_mapper = MergeIntGExpr() - for expr in exprs: - if not have_int_g_s(expr): - result_coeffs.append(expr) - result_int_gs.append(0) + int_g_cc = IntGCoefficientCollector() + for i, expr in enumerate(exprs): try: - # run the main routine to merge IntG expressions - result_coeff, result_int_g = merge_int_g_expr_mapper(expr) + int_g_coeff_map = int_g_cc(expr) + except (RuntimeError, AssertionError): + # Don't touch this expression, because it's not linear. + # FIXME: if there's ever any use case, then we can extract + # some IntGs from them. + result[i] += expr + continue + for int_g, coeff in int_g_coeff_map.items(): + if int_g == 1: + # coeff map may have some constant terms, add them to + result[i] += coeff + continue + + # convert TargetDerivative to source before checking the group + # as the target kernel has to be the same for merging + int_g = convert_target_deriv_to_source(int_g) + group = get_int_g_group(int_g) + if group in groups: + group_idx = groups.index(group) + else: + groups.append(group) + group_idx = len(groups) - 1 + exprs_per_groups.append([None]*len(exprs)) + + # move the coefficient inside + new_int_g = int_g.copy(densities=[density*coeff for density in + int_g.densities]) + prev_int_g = exprs_per_groups[group_idx][i] + if not prev_int_g: + exprs_per_groups[group_idx][i] = new_int_g + else: + merged_int_g = merge_two_int_gs(new_int_g, prev_int_g) + exprs_per_groups[group_idx][i] = merged_int_g + + # Do some simplifications after merging. Not stricty necessary + for grouped_exprs in exprs_per_groups: + for i, int_g in enumerate(grouped_exprs): + if not int_g: + continue # replace an IntG with d axis source derivatives to an IntG # with one directional source derivative - result_int_g = convert_axis_source_to_directional_source(result_int_g) + result_int_g = convert_axis_source_to_directional_source(int_g) # simplify the densities as they may become large due to pymbolic # not doing automatic simplifications unlike sympy/symengine result_int_g = result_int_g.copy( densities=simplify_densities(result_int_g.densities)) - result_coeffs.append(result_coeff) - result_int_gs.append(result_int_g) - except ValueError: - result_coeffs.append(expr) - result_int_gs.append(0) - - if source_dependent_variables is not None: - result_int_gs = reduce_number_of_fmms(result_int_gs, + grouped_exprs[i] = result_int_g + + for grouped_exprs in exprs_per_groups: + idx_to_expr_dict = {idx: expr for idx, expr in enumerate(grouped_exprs) + if expr} + filtered_group_exprs = list(idx_to_expr_dict.values()) + if source_dependent_variables is not None: + # try to reduce the number of fmms + filtered_group_exprs = reduce_number_of_fmms(filtered_group_exprs, source_dependent_variables) - result = [coeff + int_g for coeff, int_g in zip(result_coeffs, result_int_gs)] - return np.array(result, dtype=object) + for idx, expr in zip(idx_to_expr_dict.keys(), filtered_group_exprs): + result[idx] += expr + return result + + +def get_int_g_group(int_g): + return (int_g.source, int_g.target, int_g.qbx_forced_limit, + int_g.target_kernel) + + +def merge_two_int_gs(int_g_1, int_g_2): + kernel_arguments = merge_kernel_arguments(int_g_1.kernel_arguments, + int_g_2.kernel_arguments) + source_kernels = int_g_1.source_kernels + int_g_2.source_kernels + densities = int_g_1.densities + int_g_2.densities + + return int_g_1.copy( + source_kernels=tuple(source_kernels), + densities=tuple(densities), + kernel_arguments=kernel_arguments, + ) class IntGSubstitutor(IdentityMapper): @@ -150,6 +205,11 @@ def map_int_g(self, expr): return self.replacements.get(expr, expr) +class IntGCoefficientCollector(CoefficientCollector): + def map_int_g(self, expr): + return {expr: 1} + + def evalf(expr, prec=100): """evaluate an expression numerically using ``prec`` number of bits. @@ -284,6 +344,15 @@ def have_int_g_s(expr): return bool(mapper.int_g_s) +def get_int_g_s(exprs): + """Returns all :class:`~pytential.symbolic.primitives.IntG` objects + in a list of :mod:`pymbolic` expressions. + """ + get_int_g_mapper = GetIntGs() + [get_int_g_mapper(expr) for expr in exprs] + return get_int_g_mapper.int_g_s + + def filter_kernel_arguments(knls, kernel_arguments): """From a dictionary of kernel arguments, filter out arguments that are not needed for the kernels given as a list and return a new @@ -524,80 +593,6 @@ def simplify_densities(densities): return tuple(result) -class MergeIntGExpr(Mapper): - """Given an expression, return a tuple of (constant, IntG) where - the constant does not have any IntG expressions in them. - """ - def map_sum(self, expr): - result_coeff = 0 - result_int_g = 0 - for c in expr.children: - coeff, int_g = self.rec(c) - result_coeff += coeff - if int_g == 0: - continue - if result_int_g == 0: - # This is the first IntG we have encountered - result_int_g = int_g - continue - # Check that the two IntGs are compatible - if (result_int_g.source != int_g.source - or result_int_g.target != int_g.target - or result_int_g.qbx_forced_limit != int_g.qbx_forced_limit - or result_int_g.target_kernel != int_g.target_kernel): - raise ValueError - - kernel_arguments = merge_kernel_arguments(result_int_g.kernel_arguments, - int_g.kernel_arguments) - source_kernels = result_int_g.source_kernels + int_g.source_kernels - densities = result_int_g.densities + int_g.densities - new_source_kernels, new_densities = source_kernels, densities - result_int_g = result_int_g.copy( - source_kernels=tuple(new_source_kernels), - densities=tuple(new_densities), - kernel_arguments=kernel_arguments, - ) - return result_coeff, result_int_g - - def map_product(self, expr): - mult = 1 - found_int_g = None - for c in expr.children: - if not have_int_g_s(c): - mult *= c - elif found_int_g: - raise ValueError("Not a linear expression.") - else: - found_int_g = c - if not found_int_g: - return expr, 0 - else: - coeff, new_int_g = self.rec(found_int_g) - new_densities = (density * mult for density in new_int_g.densities) - return coeff*mult, new_int_g.copy(densities=new_densities) - - def map_int_g(self, expr): - new_int_g = convert_target_deriv_to_source(expr) - return 0, new_int_g - - def map_quotient(self, expr): - mult = Quotient(1, expr.denominator) - coeff, new_int_g = self.rec(expr.numerator) - new_densities = (density * mult for density in new_int_g.densities) - return coeff * mult, new_int_g.copy(densities=new_densities) - - def handle_unsupported_expression(self, expr, *args, **kwargs): - return expr, 0 - - def __call__(self, expr): - try: - return super().__call__(expr) - except NotImplementedError: - return expr, 0 - - rec = __call__ - - if __name__ == "__main__": from sumpy.kernel import (StokesletKernel, BiharmonicKernel, # noqa:F401 StressletKernel, ElasticityKernel, LaplaceKernel) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 0c5d899a7..39abeb962 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -93,16 +93,16 @@ def test_source_dependent_variable(): # Merging reduces 4 FMMs to 2 FMMs. No further reduction of FMMs. int_g3 = \ IntG(target_kernel=knl, - source_kernels=[AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), - AxisSourceDerivative(1, AxisSourceDerivative(1, knl))], - densities=[mu * nu * densities[0], mu * nu *densities[1]], + source_kernels=[AxisSourceDerivative(1, AxisSourceDerivative(1, knl)), + AxisSourceDerivative(1, AxisSourceDerivative(0, knl))], + densities=[mu * nu * densities[1], mu * nu * densities[0]], qbx_forced_limit=1) int_g4 = \ IntG(target_kernel=knl, - source_kernels=[AxisSourceDerivative(2, AxisSourceDerivative(0, knl)), - AxisSourceDerivative(2, AxisSourceDerivative(1, knl))], - densities=[densities[0], densities[1]], + source_kernels=[AxisSourceDerivative(2, AxisSourceDerivative(1, knl)), + AxisSourceDerivative(2, AxisSourceDerivative(0, knl))], + densities=[densities[1], densities[0]], qbx_forced_limit=1) assert result[0] == int_g3 @@ -131,19 +131,17 @@ def test_base_kernel_merge(): sources = [NodeCoordinateComponent(i) for i in range(dim)] - source_kernels = [ + source_kernels = list(reversed([ AxisSourceDerivative(i, AxisSourceDerivative(i, biharm_knl)) - for i in range(dim)] + for i in range(dim)])) int_g3 = IntG(target_kernel=biharm_knl, - source_kernels=source_kernels + [AxisSourceDerivative(0, biharm_knl)], - densities=[density*sources[0]*(-1.0) for i in range(dim)] - + [2*density], + source_kernels=[AxisSourceDerivative(0, biharm_knl)] + source_kernels, + densities=[2*density] + [density*sources[0]*(-1.0) for _ in range(dim)], qbx_forced_limit=1) int_g4 = IntG(target_kernel=biharm_knl, - source_kernels=source_kernels + [AxisSourceDerivative(1, biharm_knl)], - densities=[density*sources[1]*(-1.0) for i in range(dim)] - + [2*density], + source_kernels=[AxisSourceDerivative(1, biharm_knl)] + source_kernels, + densities=[2*density] + [density*sources[1]*(-1.0) for _ in range(dim)], qbx_forced_limit=1) assert int_g3 == result[0] From 1155dd720c9f073420da710469dc7ee7eec0fdfd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 12 Sep 2021 02:44:04 -0500 Subject: [PATCH 112/209] treat different kernel arguments as different --- pytential/symbolic/pde/system_utils.py | 2 +- test/test_pde_system_utils.py | 36 +++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 377a6d033..ff186937d 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -178,7 +178,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, def get_int_g_group(int_g): return (int_g.source, int_g.target, int_g.qbx_forced_limit, - int_g.target_kernel) + int_g.target_kernel, tuple(sorted(int_g.kernel_arguments.items()))) def merge_two_int_gs(int_g_1, int_g_2): diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 39abeb962..c96ab558b 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -21,7 +21,7 @@ """ from sumpy.kernel import (LaplaceKernel, AxisSourceDerivative, - AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel) + AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel, HelmholtzKernel) from pytential.symbolic.primitives import int_g_vec, IntG, NodeCoordinateComponent from pytential.symbolic.pde.system_utils import merge_int_g_exprs from pymbolic.primitives import make_sym_vector, Variable @@ -144,8 +144,38 @@ def test_base_kernel_merge(): densities=[2*density] + [density*sources[1]*(-1.0) for _ in range(dim)], qbx_forced_limit=1) - assert int_g3 == result[0] - assert int_g4 == result[1] + assert result[0] == int_g3 + assert result[1] == int_g4 + + +def test_merge_different_kernels(): + # Test different kernels Laplace, Helmholtz(k=1), Helmholtz(k=2) + dim = 3 + laplace_knl = LaplaceKernel(dim) + helmholtz_knl = HelmholtzKernel(dim) + density = make_sym_vector("sigma", 1)[0] + + int_g1 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) \ + + int_g_vec(helmholtz_knl, density, qbx_forced_limit=1, k=1) \ + + int_g_vec(AxisTargetDerivative(0, helmholtz_knl), + density, qbx_forced_limit=1, k=1) \ + + int_g_vec(helmholtz_knl, density, qbx_forced_limit=1, k=2) + + int_g2 = int_g_vec(AxisTargetDerivative(0, laplace_knl), + density, qbx_forced_limit=1) + + result = merge_int_g_exprs([int_g1, int_g2], + source_dependent_variables=[]) + + int_g3 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) \ + + IntG(target_kernel=helmholtz_knl, + source_kernels=[AxisSourceDerivative(0, helmholtz_knl), helmholtz_knl], + densities=[-density, density], + qbx_forced_limit=1, k=1) \ + + int_g_vec(helmholtz_knl, density, qbx_forced_limit=1, k=2) + + assert result[0] == int_g3 + assert result[1] == int_g2 # You can test individual routines by typing From 92ccd691700bc1fcf4b4e71ac23e341de543a432 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 12 Sep 2021 15:17:40 -0500 Subject: [PATCH 113/209] separately handle target groups and source groups --- pytential/symbolic/pde/system_utils.py | 109 +++++++++++++++++-------- 1 file changed, 74 insertions(+), 35 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index ff186937d..8513171bb 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -29,6 +29,7 @@ from pytools import (memoize_on_first_arg, generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) +from collections import defaultdict from pymbolic.mapper import WalkMapper from pymbolic.mapper.coefficient import CoefficientCollector @@ -107,11 +108,13 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, substitutor = IntGSubstitutor(replacements) exprs = [substitutor(expr) for expr in exprs] - groups = [] - exprs_per_groups = [] + # Using a dictionary instead of a set because sets are unordered + all_source_groups = dict() result = np.array([0 for _ in exprs], dtype=object) + int_gs_by_group_for_index = [] + int_g_cc = IntGCoefficientCollector() for i, expr in enumerate(exprs): try: @@ -122,6 +125,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # some IntGs from them. result[i] += expr continue + int_gs_by_group = {} for int_g, coeff in int_g_coeff_map.items(): if int_g == 1: # coeff map may have some constant terms, add them to @@ -131,29 +135,25 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # convert TargetDerivative to source before checking the group # as the target kernel has to be the same for merging int_g = convert_target_deriv_to_source(int_g) - group = get_int_g_group(int_g) - if group in groups: - group_idx = groups.index(group) - else: - groups.append(group) - group_idx = len(groups) - 1 - exprs_per_groups.append([None]*len(exprs)) - # move the coefficient inside - new_int_g = int_g.copy(densities=[density*coeff for density in + int_g = int_g.copy(densities=[density*coeff for density in int_g.densities]) - prev_int_g = exprs_per_groups[group_idx][i] - if not prev_int_g: - exprs_per_groups[group_idx][i] = new_int_g + + source_group = get_int_g_source_group(int_g) + target_group = get_int_g_target_group(int_g) + group = (source_group, target_group) + + all_source_groups[source_group] = 1 + + if group not in int_gs_by_group: + new_int_g = int_g else: - merged_int_g = merge_two_int_gs(new_int_g, prev_int_g) - exprs_per_groups[group_idx][i] = merged_int_g + prev_int_g = int_gs_by_group[group] + new_int_g = merge_two_int_gs(int_g, prev_int_g) + int_gs_by_group[group] = new_int_g - # Do some simplifications after merging. Not stricty necessary - for grouped_exprs in exprs_per_groups: - for i, int_g in enumerate(grouped_exprs): - if not int_g: - continue + # Do some simplifications after merging. Not stricty necessary + for group, int_g in int_gs_by_group.items(): # replace an IntG with d axis source derivatives to an IntG # with one directional source derivative result_int_g = convert_axis_source_to_directional_source(int_g) @@ -161,24 +161,63 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # not doing automatic simplifications unlike sympy/symengine result_int_g = result_int_g.copy( densities=simplify_densities(result_int_g.densities)) - grouped_exprs[i] = result_int_g - - for grouped_exprs in exprs_per_groups: - idx_to_expr_dict = {idx: expr for idx, expr in enumerate(grouped_exprs) - if expr} - filtered_group_exprs = list(idx_to_expr_dict.values()) - if source_dependent_variables is not None: - # try to reduce the number of fmms - filtered_group_exprs = reduce_number_of_fmms(filtered_group_exprs, + int_gs_by_group[group] = result_int_g + + int_gs_by_group_for_index.append(int_gs_by_group) + + # if source dependent variables is not given return early. + # check for (sdv is None) instead of just (sdv) because + # it can be an empty list. + if source_dependent_variables is None: + for iexpr, int_gs_by_group in enumerate(int_gs_by_group_for_index): + for int_g in int_gs_by_group.values(): + result[iexpr] += int_g + return result + + # Do the calculation for each source_group separately and assemble them + for source_group in all_source_groups.keys(): + insn_to_idx_mapping = defaultdict(list) + for idx, int_gs_by_group in enumerate(int_gs_by_group_for_index): + for group, int_g in int_gs_by_group.items(): + if group[0] != source_group: + continue + common_int_g = remove_target_identifiers(int_g) + insn_to_idx_mapping[common_int_g].append((idx, int_g)) + insns_to_reduce = list(insn_to_idx_mapping.keys()) + reduced_insns = reduce_number_of_fmms(insns_to_reduce, source_dependent_variables) - for idx, expr in zip(idx_to_expr_dict.keys(), filtered_group_exprs): - result[idx] += expr + + for insn, reduced_insn in zip(insns_to_reduce, reduced_insns): + for idx, int_g in insn_to_idx_mapping[insn]: + result[idx] += restore_target_identifiers(reduced_insn, int_g) return result -def get_int_g_group(int_g): - return (int_g.source, int_g.target, int_g.qbx_forced_limit, - int_g.target_kernel, tuple(sorted(int_g.kernel_arguments.items()))) +def get_int_g_source_group(int_g): + return (int_g.source, tuple(sorted(int_g.kernel_arguments.items()))) + + +def get_int_g_target_group(int_g): + return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel) + + +def remove_target_identifiers(int_g): + return int_g.copy(target=None, qbx_forced_limit=None, + target_kernel=int_g.target_kernel.get_base_kernel()) + + +def restore_target_identifiers(expr, orig_int_g): + int_gs = get_int_g_s([expr]) + + replacements = { + int_g: int_g.copy(target=orig_int_g.target, + qbx_forced_limit=orig_int_g.qbx_forced_limit, + target_kernel=int_g.target_kernel.replace_base_kernel( + orig_int_g.target_kernel)) + for int_g in int_gs} + + substitutor = IntGSubstitutor(replacements) + return substitutor(expr) def merge_two_int_gs(int_g_1, int_g_2): From 86763592909cedcd132ec0cf94c0740bc9007725 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 12 Sep 2021 16:05:24 -0500 Subject: [PATCH 114/209] Add comments and docstrings --- pytential/symbolic/pde/system_utils.py | 57 +++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 8513171bb..e263ab819 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -149,6 +149,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, new_int_g = int_g else: prev_int_g = int_gs_by_group[group] + # Let's merge IntGs with the same group new_int_g = merge_two_int_gs(int_g, prev_int_g) int_gs_by_group[group] = new_int_g @@ -165,8 +166,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, int_gs_by_group_for_index.append(int_gs_by_group) - # if source dependent variables is not given return early. - # check for (sdv is None) instead of just (sdv) because + # If source_dependent_variables (sdv) is not given return early. + # Check for (sdv is None) instead of just (sdv) because # it can be an empty list. if source_dependent_variables is None: for iexpr, int_gs_by_group in enumerate(int_gs_by_group_for_index): @@ -181,32 +182,76 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, for group, int_g in int_gs_by_group.items(): if group[0] != source_group: continue - common_int_g = remove_target_identifiers(int_g) + # For each output, we now have a sum of int_gs with + # different target attributes. + # for eg: {+}S + {-}D (where {x} is the QBX limit). + # We can't merge them together, because merging implies + # that everything happens at the source level and therefore + # require same target attributes. + # + # To handle this case, we can treat them separately as in + # different source base kernels, but that would imply more + # FMMs than necessary. + # + # Take the following example, + # + # [ {+}(S + D), {-}S + {avg}D, {avg}S + {-}D] + # + # If we treated the target attributes separately, then we + # will be reducing [{+}(S + D), 0, 0], [0, {-}S, {-}D], + # [0, {avg}D, {avg}S] separately which results in + # [{+}(S + D)], [{-}S, {-}D], [{avg}S, {avg}D] as + # the reduced FMMs and pytential will calculate + # [S + D, S, D] as three separate FMMs and then assemble + # the three outputs by applying target attributes. + # + # Instead, we can do S, D as two separate FMMs and get the + # result for all three outputs. To do that, we will first + # get all five expressions in the example + # [ {+}(S + D), {-}S, {avg}D, {avg}S, {-}D] + # and then remove the target attributes to get, + # [S + D, S, D]. We will reduce these and restore the target + # attributes at the end + common_int_g = remove_target_attributes(int_g) insn_to_idx_mapping[common_int_g].append((idx, int_g)) + insns_to_reduce = list(insn_to_idx_mapping.keys()) reduced_insns = reduce_number_of_fmms(insns_to_reduce, source_dependent_variables) for insn, reduced_insn in zip(insns_to_reduce, reduced_insns): for idx, int_g in insn_to_idx_mapping[insn]: - result[idx] += restore_target_identifiers(reduced_insn, int_g) + result[idx] += restore_target_attributes(reduced_insn, int_g) return result def get_int_g_source_group(int_g): + """Return a group for the *int_g* with so that all elements in that + group have the same source attributes. + """ return (int_g.source, tuple(sorted(int_g.kernel_arguments.items()))) def get_int_g_target_group(int_g): + """Return a group for the *int_g* with so that all elements in that + group have the same source attributes. + """ return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel) -def remove_target_identifiers(int_g): +def remove_target_attributes(int_g): + """Remove target attributes from *int_g* and return an expression + that is common to all expression in the same source group. + """ return int_g.copy(target=None, qbx_forced_limit=None, target_kernel=int_g.target_kernel.get_base_kernel()) -def restore_target_identifiers(expr, orig_int_g): +def restore_target_attributes(expr, orig_int_g): + """Restore target attributes from *orig_int_g* to all the + :class:`~pytential.symbolic.primitives.IntG` objects in the + input *expr*. + """ int_gs = get_int_g_s([expr]) replacements = { From f7943005dfea8425b95d1a883cd51859dea354ff Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 12 Sep 2021 16:35:43 -0500 Subject: [PATCH 115/209] remove unnecessary functions and refactor --- pytential/symbolic/pde/reduce_fmms.py | 15 ++++----- pytential/symbolic/pde/system_utils.py | 45 -------------------------- 2 files changed, 6 insertions(+), 54 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 2e3a451a1..c91594602 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -158,7 +158,7 @@ def _create_matrix(int_gs, source_dependent_variables, axis_vars): if source_expr not in source_exprs: source_exprs.append(source_expr) row += [0] - poly = _kernel_target_derivs_as_poly(source_kernel, axis_vars) + poly = _kernel_source_derivs_as_poly(source_kernel, axis_vars) row[source_exprs.index(source_expr)] += poly * to_sympy(coeff) matrix.append(row) @@ -189,7 +189,7 @@ def _check_int_gs_common(int_gs): return True -def _kernel_target_derivs_as_poly(kernel, axis_vars): +def _kernel_source_derivs_as_poly(kernel, axis_vars): """Converts a :class:`sumpy.kernel.Kernel` object to a polynomial. A :class:`sumpy.kernel.Kernel` represents a derivative operator and the derivative operator is converted to a polynomial with @@ -198,17 +198,14 @@ def _kernel_target_derivs_as_poly(kernel, axis_vars): For eg: for target y and source x the derivative operator, d/dx_1 dy_2 + d/dy_1 is converted to -y_2 * y_1 + y_1. """ - if isinstance(kernel, AxisTargetDerivative): - poly = _kernel_target_derivs_as_poly(kernel.inner_kernel, axis_vars) - return axis_vars[kernel.axis]*poly - elif isinstance(kernel, AxisSourceDerivative): - poly = _kernel_target_derivs_as_poly(kernel.inner_kernel, axis_vars) + if isinstance(kernel, AxisSourceDerivative): + poly = _kernel_source_derivs_as_poly(kernel.inner_kernel, axis_vars) return -axis_vars[kernel.axis]*poly return 1 def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): - """This does the opposite of :func:`_kernel_target_derivs_as_poly` + """This does the opposite of :func:`_kernel_source_derivs_as_poly` and converts a polynomial back to a source derivative operator. First it is converted to a :class:`sumpy.kernel.Kernel` and then to a :class:`~pytential.symbolic.primitives.IntG`. @@ -231,7 +228,7 @@ def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): - """This does the opposite of :func:`_kernel_target_derivs_as_poly` + """This does the opposite of :func:`_kernel_source_derivs_as_poly` and converts a polynomial back to a target derivative operator. It is applied to a :class:`~pytential.symbolic.primitives.IntG` object and returns a new instance. diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index e263ab819..a6e9b546d 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -499,51 +499,6 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): return result -def _convert_kernel_to_poly(kernel, axis_vars): - if isinstance(kernel, AxisTargetDerivative): - poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) - return axis_vars[kernel.axis]*poly - elif isinstance(kernel, AxisSourceDerivative): - poly = _convert_kernel_to_poly(kernel.inner_kernel, axis_vars) - return -axis_vars[kernel.axis]*poly - return 1 - - -def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): - from pymbolic.interop.sympy import SympyToPymbolicMapper - to_pymbolic = SympyToPymbolicMapper() - - orig_kernel = orig_int_g.source_kernels[0] - source_kernels = [] - densities = [] - for monom, coeff in poly.terms(): - kernel = orig_kernel - for idim, rep in enumerate(monom): - for _ in range(rep): - kernel = AxisSourceDerivative(idim, kernel) - source_kernels.append(kernel) - densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) - return orig_int_g.copy(source_kernels=tuple(source_kernels), - densities=tuple(densities)) - - -def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): - from pymbolic.interop.sympy import SympyToPymbolicMapper - to_pymbolic = SympyToPymbolicMapper() - - result = 0 - for monom, coeff in poly.terms(): - kernel = orig_int_g.target_kernel - for idim, rep in enumerate(monom): - for _ in range(rep): - kernel = AxisTargetDerivative(idim, kernel) - result += orig_int_g.copy(target_kernel=kernel, - source_kernels=rhs_int_g.source_kernels, - densities=rhs_int_g.densities) * to_pymbolic(coeff) - - return result - - def convert_target_multiplier_to_source(int_g): """Convert an IntG with TargetMultiplier to an sum of IntGs without TargetMultiplier and only source dependent transformations From 89ffcf6d2173a91b2a29b09e8d41c4f5a82e1a32 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 12 Sep 2021 16:55:04 -0500 Subject: [PATCH 116/209] fix example in docstrings --- pytential/symbolic/pde/reduce_fmms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index c91594602..8f893c29e 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -195,8 +195,8 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): and the derivative operator is converted to a polynomial with variables given by `axis_vars`. - For eg: for target y and source x the derivative operator, - d/dx_1 dy_2 + d/dy_1 is converted to -y_2 * y_1 + y_1. + For eg: for source x the derivative operator, + d/dx_1 dx_2 + d/dx_1 is converted to x_2 * x_1 + x_1. """ if isinstance(kernel, AxisSourceDerivative): poly = _kernel_source_derivs_as_poly(kernel.inner_kernel, axis_vars) From c0d1b9cc9f2f836feabf320a72b70e96b1e07770 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 12 Sep 2021 19:02:51 -0500 Subject: [PATCH 117/209] test different qbx_forced_limit --- test/test_pde_system_utils.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index c96ab558b..fce045b87 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -178,6 +178,38 @@ def test_merge_different_kernels(): assert result[1] == int_g2 +def test_merge_different_qbx_forced_limit(): + # Test different kernels Laplace, Helmholtz(k=1), Helmholtz(k=2) + dim = 3 + laplace_knl = LaplaceKernel(dim) + density = make_sym_vector("sigma", 1)[0] + + + int_g1 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) + int_g2 = int_g_vec(AxisTargetDerivative(0, laplace_knl), + density, qbx_forced_limit=1) + + int_g3 = int_g2 + int_g1 + int_g4 = int_g1.copy(qbx_forced_limit=2) + int_g2.copy(qbx_forced_limit=-2) + int_g5 = int_g1.copy(qbx_forced_limit=-2) + int_g2.copy(qbx_forced_limit=2) + + result = merge_int_g_exprs([int_g3, int_g4, int_g5], + source_dependent_variables=[]) + + int_g6 = int_g_vec(laplace_knl, -density, qbx_forced_limit=1) + int_g7 = int_g_vec(AxisTargetDerivative(0, laplace_knl), + -density, qbx_forced_limit=1) + int_g8 = int_g7 * (-1) + int_g6 * (-1) + int_g9 = int_g6.copy(qbx_forced_limit=2) * (-1) \ + + int_g7.copy(qbx_forced_limit=-2) * (-1) + int_g10 = int_g6.copy(qbx_forced_limit=-2) * (-1) \ + + int_g7.copy(qbx_forced_limit=2) * (-1) + + assert result[0] == int_g8 + assert result[1] == int_g9 + assert result[2] == int_g10 + + # You can test individual routines by typing # $ python test_pde_system_tools.py 'test_reduce_number_of_fmms()' From b189a27342e2e3057006fde75caf773ac1c512ee Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 13 Sep 2021 16:37:26 -0500 Subject: [PATCH 118/209] Support DirectionalSourceDerivatives --- pytential/symbolic/pde/reduce_fmms.py | 12 +++- pytential/symbolic/pde/system_utils.py | 87 +++++++++++++++++++++++--- test/test_pde_system_utils.py | 45 ++++++++++--- 3 files changed, 128 insertions(+), 16 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 8f893c29e..2d3d698f8 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -20,7 +20,8 @@ THE SOFTWARE. """ -from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative) +from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, + KernelWrapper) from pymbolic.interop.sympy import PymbolicToSympyMapper, SympyToPymbolicMapper from pymbolic.mapper import Mapper @@ -201,6 +202,8 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): if isinstance(kernel, AxisSourceDerivative): poly = _kernel_source_derivs_as_poly(kernel.inner_kernel, axis_vars) return -axis_vars[kernel.axis]*poly + if isinstance(kernel, KernelWrapper): + raise ValueError return 1 @@ -278,6 +281,13 @@ def map_algebraic_leaf(self, expr): else: return {1: expr} + def map_subscript(self, expr): + if expr in self.source_dependent_variables or \ + expr.aggregate in self.source_dependent_variables: + return {expr: 1} + else: + return {1: expr} + def map_constant(self, expr): return {1: expr} diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index a6e9b546d..93095685e 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -33,6 +33,7 @@ from pymbolic.mapper import WalkMapper from pymbolic.mapper.coefficient import CoefficientCollector +from pymbolic.primitives import Variable from pytential.symbolic.primitives import IntG, NodeCoordinateComponent from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand @@ -108,6 +109,16 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, substitutor = IntGSubstitutor(replacements) exprs = [substitutor(expr) for expr in exprs] + differing_kernel_arguments = set() + kernel_arguments = {} + for int_g in int_g_s: + for k, v in int_g.kernel_arguments.items(): + if k not in kernel_arguments: + kernel_arguments[k] = v + else: + if kernel_arguments[k] != v: + differing_kernel_arguments.add(k) + # Using a dictionary instead of a set because sets are unordered all_source_groups = dict() @@ -132,6 +143,11 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, result[i] += coeff continue + # convert DirectionalSourceDerivative to AxisSourceDerivative + # as kernel arguments need to be the same for merging + if source_dependent_variables is not None: + int_g = convert_directional_source_to_axis_source(int_g, + source_dependent_variables) # convert TargetDerivative to source before checking the group # as the target kernel has to be the same for merging int_g = convert_target_deriv_to_source(int_g) @@ -139,7 +155,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, int_g = int_g.copy(densities=[density*coeff for density in int_g.densities]) - source_group = get_int_g_source_group(int_g) + source_group = get_int_g_source_group(int_g, differing_kernel_arguments) target_group = get_int_g_target_group(int_g) group = (source_group, target_group) @@ -157,11 +173,12 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, for group, int_g in int_gs_by_group.items(): # replace an IntG with d axis source derivatives to an IntG # with one directional source derivative - result_int_g = convert_axis_source_to_directional_source(int_g) + # TODO: reenable this later + # result_int_g = convert_axis_source_to_directional_source(int_g) # simplify the densities as they may become large due to pymbolic # not doing automatic simplifications unlike sympy/symengine - result_int_g = result_int_g.copy( - densities=simplify_densities(result_int_g.densities)) + result_int_g = int_g.copy( + densities=simplify_densities(int_g.densities)) int_gs_by_group[group] = result_int_g int_gs_by_group_for_index.append(int_gs_by_group) @@ -225,11 +242,19 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, return result -def get_int_g_source_group(int_g): +def get_int_g_source_group(int_g, differing_kernel_arguments): """Return a group for the *int_g* with so that all elements in that group have the same source attributes. """ - return (int_g.source, tuple(sorted(int_g.kernel_arguments.items()))) + def get_hashable(arg): + if hasattr(arg, "__iter__"): + return tuple(arg) + return arg + + args = tuple((k, get_hashable(v)) for k, v in sorted( + int_g.kernel_arguments.items()) if k in + differing_kernel_arguments) + return (int_g.source, args, int_g.target_kernel.get_base_kernel()) def get_int_g_target_group(int_g): @@ -569,10 +594,52 @@ def convert_target_deriv_to_source(int_g): source_kernels=tuple(source_kernels)) +def convert_directional_source_to_axis_source(int_g, source_dependent_variables): + """Convert an IntG with a DirectionalSourceDerivative instance + to an IntG with d AxisSourceDerivative instances. + """ + source_kernels = [] + densities = [] + for source_kernel, density in zip(int_g.source_kernels, int_g.densities): + knl_result = _convert_directional_source_knl_to_axis_source(source_kernel, + source_dependent_variables) + for knl, coeff in knl_result: + source_kernels.append(knl) + densities.append(coeff * density) + return int_g.copy(source_kernels=tuple(source_kernels), + densities=tuple(densities)) + + +def _convert_directional_source_knl_to_axis_source(knl, source_dependent_variables): + if isinstance(knl, DirectionalSourceDerivative): + dim = knl.dim + from pymbolic import make_sym_vector + dir_vec = make_sym_vector(knl.dir_vec_name, dim) + if Variable(knl.dir_vec_name) not in source_dependent_variables: + raise ValueError(f"{knl.dir_vec_name} not given in " + "source_dependent_variables, but is dependent on source") + + res = [] + inner_result = _convert_directional_source_knl_to_axis_source( + knl.inner_kernel, source_dependent_variables) + for inner_knl, coeff in inner_result: + for d in range(dim): + res.append((AxisSourceDerivative(d, inner_knl), coeff*dir_vec[d])) + return res + elif isinstance(knl, KernelWrapper): + inner_result = _convert_directional_source_knl_to_axis_source( + knl.inner_kernel, source_dependent_variables) + return [(knl.replace_inner_kernel(inner_knl), coeff) for + inner_knl, coeff in inner_result] + else: + return [(knl, 1)] + + def convert_axis_source_to_directional_source(int_g): """Convert an IntG with d AxisSourceDerivative instances to an IntG with one DirectionalSourceDerivative instance. """ + from pytential.symbolic.primitives import _DIR_VEC_NAME if not isinstance(int_g, IntG): return int_g knls = list(int_g.source_kernels) @@ -590,7 +657,13 @@ def convert_axis_source_to_directional_source(int_g): return int_g base_knl = base_knls.pop() kernel_arguments = int_g.kernel_arguments.copy() - name = "generated_dir_vec" + + name = _DIR_VEC_NAME + count = 0 + while name in kernel_arguments: + name = _DIR_VEC_NAME + f"_{count}" + count += 1 + kernel_arguments[name] = \ np.array(int_g.densities, dtype=np.object) res = int_g.copy( diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index fce045b87..cc3b2bb2a 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -22,9 +22,11 @@ from sumpy.kernel import (LaplaceKernel, AxisSourceDerivative, AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel, HelmholtzKernel) -from pytential.symbolic.primitives import int_g_vec, IntG, NodeCoordinateComponent +from pytential.symbolic.primitives import (int_g_vec, D, IntG, + NodeCoordinateComponent) from pytential.symbolic.pde.system_utils import merge_int_g_exprs from pymbolic.primitives import make_sym_vector, Variable +import pytest def test_reduce_number_of_fmms(): @@ -179,17 +181,14 @@ def test_merge_different_kernels(): def test_merge_different_qbx_forced_limit(): - # Test different kernels Laplace, Helmholtz(k=1), Helmholtz(k=2) dim = 3 laplace_knl = LaplaceKernel(dim) density = make_sym_vector("sigma", 1)[0] - int_g1 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) - int_g2 = int_g_vec(AxisTargetDerivative(0, laplace_knl), - density, qbx_forced_limit=1) + int_g2 = int_g1.copy(target_kernel=AxisTargetDerivative(0, laplace_knl)) - int_g3 = int_g2 + int_g1 + int_g3, = merge_int_g_exprs([int_g2 + int_g1]) int_g4 = int_g1.copy(qbx_forced_limit=2) + int_g2.copy(qbx_forced_limit=-2) int_g5 = int_g1.copy(qbx_forced_limit=-2) + int_g2.copy(qbx_forced_limit=2) @@ -197,8 +196,7 @@ def test_merge_different_qbx_forced_limit(): source_dependent_variables=[]) int_g6 = int_g_vec(laplace_knl, -density, qbx_forced_limit=1) - int_g7 = int_g_vec(AxisTargetDerivative(0, laplace_knl), - -density, qbx_forced_limit=1) + int_g7 = int_g6.copy(target_kernel=AxisTargetDerivative(0, laplace_knl)) int_g8 = int_g7 * (-1) + int_g6 * (-1) int_g9 = int_g6.copy(qbx_forced_limit=2) * (-1) \ + int_g7.copy(qbx_forced_limit=-2) * (-1) @@ -210,6 +208,37 @@ def test_merge_different_qbx_forced_limit(): assert result[2] == int_g10 +def test_merge_directional_source(): + from pymbolic.primitives import Variable + dim = 3 + laplace_knl = LaplaceKernel(dim) + density = Variable("density") + dsource = Variable("dsource_vec") + + int_g1 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) + int_g2 = D(laplace_knl, density, qbx_forced_limit=1) + + with pytest.raises(ValueError): + result = merge_int_g_exprs([int_g1 + int_g2], + source_dependent_variables=[]) + + result = merge_int_g_exprs([int_g1 + int_g2], + source_dependent_variables=[dsource]) + + source_kernels = [AxisSourceDerivative(d, laplace_knl) + for d in range(dim)] + [laplace_knl] + densities = [density*dsource[d] for d in range(dim)] + [density] + int_g3 = int_g2.copy(source_kernels=source_kernels, densities=densities) + + assert result[0] == int_g3 + + result = merge_int_g_exprs([int_g1 + int_g2]) + int_g4 = int_g2.copy( + source_kernels=int_g2.source_kernels + int_g1.source_kernels, + densities=(density, density)) + assert result[0] == int_g4 + + # You can test individual routines by typing # $ python test_pde_system_tools.py 'test_reduce_number_of_fmms()' From b3e5092f560010a0e8535d0e74508976902bdd9d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 13 Sep 2021 17:26:24 -0500 Subject: [PATCH 119/209] Handle unsupported expression in IntGCoefficientCollector --- pytential/symbolic/pde/system_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 93095685e..e6ea8b3e3 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -126,7 +126,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, int_gs_by_group_for_index = [] - int_g_cc = IntGCoefficientCollector() + int_g_cc = IntGCoefficientCollector({}) for i, expr in enumerate(exprs): try: int_g_coeff_map = int_g_cc(expr) @@ -318,6 +318,9 @@ class IntGCoefficientCollector(CoefficientCollector): def map_int_g(self, expr): return {expr: 1} + def handle_unsupported_expression(self, expr, *args, **kwargs): + return {1: expr} + def evalf(expr, prec=100): """evaluate an expression numerically using ``prec`` From a12d6b789c02f4fd7f4acd029ce33e8166440a1a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 13 Sep 2021 17:46:22 -0500 Subject: [PATCH 120/209] Add a test --- pytential/symbolic/pde/system_utils.py | 5 ++++- test/test_pde_system_utils.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index e6ea8b3e3..18b43ad50 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -126,7 +126,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, int_gs_by_group_for_index = [] - int_g_cc = IntGCoefficientCollector({}) + int_g_cc = IntGCoefficientCollector() for i, expr in enumerate(exprs): try: int_g_coeff_map = int_g_cc(expr) @@ -315,6 +315,9 @@ def map_int_g(self, expr): class IntGCoefficientCollector(CoefficientCollector): + def __init__(self): + super().__init__({}) + def map_int_g(self, expr): return {expr: 1} diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index cc3b2bb2a..d3ae209d4 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -36,8 +36,8 @@ def test_reduce_number_of_fmms(): mu = Variable("mu") int_g1 = \ - int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), - densities[0] * mu, qbx_forced_limit=1) + \ + mu * int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), + densities[0], qbx_forced_limit=1) + \ int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(1, knl)), densities[1] * mu, qbx_forced_limit=1) From d3855298b8018aa7d3ba9d6fa04cfb40fe81d3ac Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 13 Sep 2021 18:35:02 -0500 Subject: [PATCH 121/209] fix map_algebraic_leaf --- pytential/symbolic/pde/system_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 18b43ad50..ab8ac71f1 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -321,9 +321,11 @@ def __init__(self): def map_int_g(self, expr): return {expr: 1} - def handle_unsupported_expression(self, expr, *args, **kwargs): + def map_algebraic_leaf(self, expr, *args, **kwargs): return {1: expr} + handle_unsupported_expression = map_algebraic_leaf + def evalf(expr, prec=100): """evaluate an expression numerically using ``prec`` From afb3e084a6644a0fc4595526eacf70d68d429795 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 14 Sep 2021 17:14:26 -0500 Subject: [PATCH 122/209] explain sympy.EX --- pytential/symbolic/pde/reduce_fmms.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 2d3d698f8..405b0ce87 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -340,9 +340,12 @@ def map_power(self, expr): def _syzygy_module(m, generators): - """Takes as input a module of polynomials with domain `sympy.EX` represented - as a matrix and returns the syzygy module as a matrix of polynomials in the - same domain. + """Takes as input a module of polynomials with domain :class:`sympy.EX` + represented as a matrix and returns the syzygy module as a matrix of polynomials + in the same domain. + Using :class:`sympy.EX` because that represents the domain with any symbolic + element. Usually we need an Integer or Rational domain, but since there can be + unrelated symbols like *mu* in the expression, we need to use a symbolic domain. """ from sympy.polys.orderings import grevlex From 4ed556e70d5221e1458aa794e94802688798a33a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 14 Sep 2021 17:14:42 -0500 Subject: [PATCH 123/209] explain syzygy_module --- pytential/symbolic/pde/reduce_fmms.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 405b0ce87..c452e9cf2 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -364,9 +364,6 @@ def _convert_to_matrix(module, *generators): for i in range(m.shape[1])] column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] - if not column_syzygy_modules: - raise ValueError - intersection = functools.reduce(lambda x, y: x.intersect(y), column_syzygy_modules) @@ -376,8 +373,18 @@ def _convert_to_matrix(module, *generators): def _factor_left(mat, axis_vars): - """Return the left hand side of the factorisation of the matrix""" - return _syzygy_module(_syzygy_module(mat, axis_vars).T, axis_vars).T + """Return the left hand side of the factorisation of the matrix + For a matrix M, we want to find a factorisation such that M = L R + with minimum number of columns of L. + To get a good factorisation, what we do is first find a matrix + such that S M = 0 where S is the syzygy module converted to a matrix. + Then, M.T S.T = 0 which implies that M.T is in the space spanned by + the syzygy module of S.T and to get M we get the transpose of that. + """ + first_syzygy_module = _syzygy_module(mat, axis_vars) + if len(first_syzygy_module) == 0: + raise ValueError("could not find a factorization") + return _syzygy_module(first_syzygy_module.T, axis_vars).T def _factor_right(mat, factor_left): From 6ef9fd84b2ee6c656f27c34f1c59717e2cf61426 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 14 Sep 2021 17:18:51 -0500 Subject: [PATCH 124/209] explain syzygy --- pytential/symbolic/pde/reduce_fmms.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index c452e9cf2..0d10317f0 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -342,7 +342,8 @@ def map_power(self, expr): def _syzygy_module(m, generators): """Takes as input a module of polynomials with domain :class:`sympy.EX` represented as a matrix and returns the syzygy module as a matrix of polynomials - in the same domain. + in the same domain. The syzygy module *S* that is returned as a matrix + satisfies S m = 0. Using :class:`sympy.EX` because that represents the domain with any symbolic element. Usually we need an Integer or Rational domain, but since there can be unrelated symbols like *mu* in the expression, we need to use a symbolic domain. From 35d63f5a0fc8a0fd4af16b35dc4f08c4d06b8f57 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 19 Sep 2021 23:58:00 -0500 Subject: [PATCH 125/209] explain target dependent variables and common properties --- pytential/symbolic/pde/reduce_fmms.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 0d10317f0..b2b5ad8a6 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -61,15 +61,17 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): on source. For eg: densities, source derivative vectors. Note: there is no argument for target-dependent variables as the algorithm - assumes that there are no target-dependent-variables passed to this function. - (where a "target-dependent variable" represents a function discretized on the - targets) + assumes that there are no target-dependent variables passed to this function. + (where a "source/target-dependent variable" is a symbolic variable that evaluates + to a vector discretized on the sources/targets) """ dim = int_gs[0].target_kernel.dim axis_vars = sympy.symbols(f"_x0:{dim}") - # A high level driver for this function should send int_gs that are common. + # A high level driver for this function should send int_gs that share all the + # properties except for the densities, source transformations and target + # transformations. assert _check_int_gs_common(int_gs) try: From d87a0ba21fda883687707e9920bc07856ade619d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:03:06 -0500 Subject: [PATCH 126/209] rhs_mat -> right_factor and fix docs --- pytential/symbolic/pde/reduce_fmms.py | 52 ++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index b2b5ad8a6..d36aa186e 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -83,55 +83,57 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): mat = sympy.Matrix(mat) # Factor the matrix into two try: - lhs_mat = _factor_left(mat, axis_vars) - rhs_mat = _factor_right(mat, lhs_mat) + left_factor = _factor_left(mat, axis_vars) + right_factor = _factor_right(mat, left_factor) except ValueError: return int_gs # If there are n inputs and m outputs, # - # - matrix = R^{m x n}, - # - LHS = R^{m x k}, - # - RHS = R^{k x n}. + # - matrix: R^{m x n}, + # - LHS: R^{m x k}, + # - RHS: R^{k x n}. # - # If k is not greater than or equal to n we are gaining nothing. + # If k is greater than or equal to n we are gaining nothing. # Return as is. - if rhs_mat.shape[0] >= mat.shape[0]: + if right_factor.shape[0] >= mat.shape[0]: return int_gs - # Create SymPy Polynomial objects - rhs_mat = rhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) - lhs_mat = lhs_mat.applyfunc(lambda x: x.as_poly(*axis_vars, domain=sympy.EX)) + # cast to SymPy Polynomial objects + right_factor = right_factor.applyfunc(lambda x: x.as_poly(*axis_vars, + domain=sympy.EX)) + left_factor = left_factor.applyfunc(lambda x: x.as_poly(*axis_vars, + domain=sympy.EX)) base_kernel = int_gs[0].source_kernels[0].get_base_kernel() base_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) - # Convert each element in the RHS matrix to IntGs - rhs_mat_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) - for poly in row] for row in rhs_mat.tolist()] + # Convert each element in the right factor to IntGs + source_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) + for poly in row] for row in right_factor.tolist()] - # For each row in the RHS matrix, merge the IntGs to one IntG + # For each row in the right factor, merge the IntGs to one IntG # to get a total of k IntGs. - rhs_int_gs = [] - for i in range(rhs_mat.shape[0]): + source_int_gs_merged = [] + for i in range(right_factor.shape[0]): source_kernels = [] densities = [] - for j in range(rhs_mat.shape[1]): + for j in range(right_factor.shape[1]): new_densities = [density * source_exprs[j] for density in - rhs_mat_int_gs[i][j].densities] - source_kernels.extend(rhs_mat_int_gs[i][j].source_kernels) + source_int_gs[i][j].densities] + source_kernels.extend(source_int_gs[i][j].source_kernels) densities.extend(new_densities) - rhs_int_gs.append(rhs_mat_int_gs[i][0].copy( + source_int_gs_merged.append(source_int_gs[i][0].copy( source_kernels=tuple(source_kernels), densities=tuple(densities))) # Now that we have the IntG expressions depending on the source # we now have to attach the target dependent derivatives. - res = [0]*lhs_mat.shape[0] - for i in range(lhs_mat.shape[0]): - for j in range(lhs_mat.shape[1]): - res[i] += _convert_target_poly_to_int_g(lhs_mat[i, j], - int_gs[i], rhs_int_gs[j]) + res = [0]*left_factor.shape[0] + for i in range(left_factor.shape[0]): + for j in range(left_factor.shape[1]): + res[i] += _convert_target_poly_to_int_g(left_factor[i, j], + int_gs[i], source_int_gs_merged[j]) return res From cf38f70832749e8cc94110a175e21cedde306992 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:05:37 -0500 Subject: [PATCH 127/209] _convert_target_poly_to_int_g -> _convert_target_poly_to_int_g_derivs, update docs --- pytential/symbolic/pde/reduce_fmms.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index d36aa186e..d5ced7bb8 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -109,9 +109,9 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): base_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) - # Convert each element in the right factor to IntGs - source_int_gs = [[_convert_source_poly_to_int_g(poly, base_int_g, axis_vars) - for poly in row] for row in right_factor.tolist()] + # Convert polynomials back to IntGs with source derivatives + source_int_gs = [[_convert_source_poly_to_int_g_derivs(poly, base_int_g, + axis_vars) for poly in row] for row in right_factor.tolist()] # For each row in the right factor, merge the IntGs to one IntG # to get a total of k IntGs. @@ -132,7 +132,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): res = [0]*left_factor.shape[0] for i in range(left_factor.shape[0]): for j in range(left_factor.shape[1]): - res[i] += _convert_target_poly_to_int_g(left_factor[i, j], + res[i] += _convert_target_poly_to_int_g_derivs(left_factor[i, j], int_gs[i], source_int_gs_merged[j]) return res @@ -211,7 +211,7 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): return 1 -def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): +def _convert_source_poly_to_int_g_derivs(poly, orig_int_g, axis_vars): """This does the opposite of :func:`_kernel_source_derivs_as_poly` and converts a polynomial back to a source derivative operator. First it is converted to a :class:`sumpy.kernel.Kernel` @@ -234,7 +234,7 @@ def _convert_source_poly_to_int_g(poly, orig_int_g, axis_vars): densities=tuple(simplify_densities(densities))) -def _convert_target_poly_to_int_g(poly, orig_int_g, rhs_int_g): +def _convert_target_poly_to_int_g_derivs(poly, orig_int_g, rhs_int_g): """This does the opposite of :func:`_kernel_source_derivs_as_poly` and converts a polynomial back to a target derivative operator. It is applied to a :class:`~pytential.symbolic.primitives.IntG` From 2d659b08251fb864ab47da9d71bb7bef126092bb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:09:28 -0500 Subject: [PATCH 128/209] explain why (-1) --- pytential/symbolic/pde/reduce_fmms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index d5ced7bb8..1dfde5051 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -229,6 +229,7 @@ def _convert_source_poly_to_int_g_derivs(poly, orig_int_g, axis_vars): for _ in range(rep): kernel = AxisSourceDerivative(idim, kernel) source_kernels.append(kernel) + # (-1) below is because d/dx f(c - x) = - f'(c - x) densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) return orig_int_g.copy(source_kernels=tuple(source_kernels), densities=tuple(simplify_densities(densities))) From bf065ecd5305aee48f1386680ab2f01f9ff13b12 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:17:21 -0500 Subject: [PATCH 129/209] reorganize functions and add vim markers --- pytential/symbolic/pde/reduce_fmms.py | 176 ++++++++++++++------------ 1 file changed, 96 insertions(+), 80 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 1dfde5051..38e91f3df 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -36,6 +36,8 @@ ) +# {{{ Reduce number of FMMs - main routine + def reduce_number_of_fmms(int_gs, source_dependent_variables): """ Reduce the number of FMMs needed for a system of @@ -138,6 +140,28 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): return res +# }}} + +# {{{ convert IntG expressions to a matrix + +def _check_int_gs_common(int_gs): + """Checks that the :class:`~pytential.symbolic.primtive.IntG` objects + have the same base kernel and other properties that would allow + merging them. + """ + base_kernel = int_gs[0].source_kernels[0].get_base_kernel() + common_int_g = int_gs[0].copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)) + for int_g in int_gs: + for source_kernel in int_g.source_kernels: + if source_kernel.get_base_kernel() != base_kernel: + return False + if common_int_g != int_g.copy(target_kernel=base_kernel, + source_kernels=(base_kernel,), densities=(1,)): + return False + return True + + def _create_matrix(int_gs, source_dependent_variables, axis_vars): """Create a matrix from a list of :class:`~pytential.symbolic.primitives.IntG` objects and returns the matrix and the expressions corresponding to each column. @@ -176,86 +200,6 @@ def _create_matrix(int_gs, source_dependent_variables, axis_vars): return matrix, source_exprs -def _check_int_gs_common(int_gs): - """Checks that the :class:`~pytential.symbolic.primtive.IntG` objects - have the same base kernel and other properties that would allow - merging them. - """ - base_kernel = int_gs[0].source_kernels[0].get_base_kernel() - common_int_g = int_gs[0].copy(target_kernel=base_kernel, - source_kernels=(base_kernel,), densities=(1,)) - for int_g in int_gs: - for source_kernel in int_g.source_kernels: - if source_kernel.get_base_kernel() != base_kernel: - return False - if common_int_g != int_g.copy(target_kernel=base_kernel, - source_kernels=(base_kernel,), densities=(1,)): - return False - return True - - -def _kernel_source_derivs_as_poly(kernel, axis_vars): - """Converts a :class:`sumpy.kernel.Kernel` object to a polynomial. - A :class:`sumpy.kernel.Kernel` represents a derivative operator - and the derivative operator is converted to a polynomial with - variables given by `axis_vars`. - - For eg: for source x the derivative operator, - d/dx_1 dx_2 + d/dx_1 is converted to x_2 * x_1 + x_1. - """ - if isinstance(kernel, AxisSourceDerivative): - poly = _kernel_source_derivs_as_poly(kernel.inner_kernel, axis_vars) - return -axis_vars[kernel.axis]*poly - if isinstance(kernel, KernelWrapper): - raise ValueError - return 1 - - -def _convert_source_poly_to_int_g_derivs(poly, orig_int_g, axis_vars): - """This does the opposite of :func:`_kernel_source_derivs_as_poly` - and converts a polynomial back to a source derivative - operator. First it is converted to a :class:`sumpy.kernel.Kernel` - and then to a :class:`~pytential.symbolic.primitives.IntG`. - """ - from pytential.symbolic.pde.system_utils import simplify_densities - to_pymbolic = SympyToPymbolicMapper() - - orig_kernel = orig_int_g.source_kernels[0] - source_kernels = [] - densities = [] - for monom, coeff in poly.terms(): - kernel = orig_kernel - for idim, rep in enumerate(monom): - for _ in range(rep): - kernel = AxisSourceDerivative(idim, kernel) - source_kernels.append(kernel) - # (-1) below is because d/dx f(c - x) = - f'(c - x) - densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) - return orig_int_g.copy(source_kernels=tuple(source_kernels), - densities=tuple(simplify_densities(densities))) - - -def _convert_target_poly_to_int_g_derivs(poly, orig_int_g, rhs_int_g): - """This does the opposite of :func:`_kernel_source_derivs_as_poly` - and converts a polynomial back to a target derivative - operator. It is applied to a :class:`~pytential.symbolic.primitives.IntG` - object and returns a new instance. - """ - to_pymbolic = SympyToPymbolicMapper() - - result = 0 - for monom, coeff in poly.terms(): - kernel = orig_int_g.target_kernel - for idim, rep in enumerate(monom): - for _ in range(rep): - kernel = AxisTargetDerivative(idim, kernel) - result += orig_int_g.copy(target_kernel=kernel, - source_kernels=rhs_int_g.source_kernels, - densities=rhs_int_g.densities) * to_pymbolic(coeff) - - return result - - class CoefficientCollector(Mapper): """From a density expression, extracts expressions that need to be evaluated for each source and coefficients for each expression. @@ -344,6 +288,26 @@ def map_power(self, expr): rec = __call__ +def _kernel_source_derivs_as_poly(kernel, axis_vars): + """Converts a :class:`sumpy.kernel.Kernel` object to a polynomial. + A :class:`sumpy.kernel.Kernel` represents a derivative operator + and the derivative operator is converted to a polynomial with + variables given by `axis_vars`. + + For eg: for source x the derivative operator, + d/dx_1 dx_2 + d/dx_1 is converted to x_2 * x_1 + x_1. + """ + if isinstance(kernel, AxisSourceDerivative): + poly = _kernel_source_derivs_as_poly(kernel.inner_kernel, axis_vars) + return -axis_vars[kernel.axis]*poly + if isinstance(kernel, KernelWrapper): + raise ValueError + return 1 + +# }}} + +# {{{ factor the matrix + def _syzygy_module(m, generators): """Takes as input a module of polynomials with domain :class:`sympy.EX` represented as a matrix and returns the syzygy module as a matrix of polynomials @@ -408,3 +372,55 @@ def _factor_right(mat, factor_left): factor_right.append(row) factor_right = sympy.Matrix(factor_right).T return factor_right + +# }}} + +# {{{ convert factors back into IntGs + +def _convert_source_poly_to_int_g_derivs(poly, orig_int_g, axis_vars): + """This does the opposite of :func:`_kernel_source_derivs_as_poly` + and converts a polynomial back to a source derivative + operator. First it is converted to a :class:`sumpy.kernel.Kernel` + and then to a :class:`~pytential.symbolic.primitives.IntG`. + """ + from pytential.symbolic.pde.system_utils import simplify_densities + to_pymbolic = SympyToPymbolicMapper() + + orig_kernel = orig_int_g.source_kernels[0] + source_kernels = [] + densities = [] + for monom, coeff in poly.terms(): + kernel = orig_kernel + for idim, rep in enumerate(monom): + for _ in range(rep): + kernel = AxisSourceDerivative(idim, kernel) + source_kernels.append(kernel) + # (-1) below is because d/dx f(c - x) = - f'(c - x) + densities.append(to_pymbolic(coeff) * (-1)**sum(monom)) + return orig_int_g.copy(source_kernels=tuple(source_kernels), + densities=tuple(simplify_densities(densities))) + + +def _convert_target_poly_to_int_g_derivs(poly, orig_int_g, rhs_int_g): + """This does the opposite of :func:`_kernel_source_derivs_as_poly` + and converts a polynomial back to a target derivative + operator. It is applied to a :class:`~pytential.symbolic.primitives.IntG` + object and returns a new instance. + """ + to_pymbolic = SympyToPymbolicMapper() + + result = 0 + for monom, coeff in poly.terms(): + kernel = orig_int_g.target_kernel + for idim, rep in enumerate(monom): + for _ in range(rep): + kernel = AxisTargetDerivative(idim, kernel) + result += orig_int_g.copy(target_kernel=kernel, + source_kernels=rhs_int_g.source_kernels, + densities=rhs_int_g.densities) * to_pymbolic(coeff) + + return result + +# }}} + +# vim: fdm=marker From 8e58d39e4d133e266f770ffb849613171e65f852 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:20:56 -0500 Subject: [PATCH 130/209] explain difference between pymbolic's coefficientcollector and pytential's --- pytential/symbolic/pde/reduce_fmms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 38e91f3df..0dd829067 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -206,6 +206,11 @@ class CoefficientCollector(Mapper): For eg: when this mapper is given as ``s*(s + 2) + 3`` input, it returns {s**2: 1, s: 2, 1: 3}. + + This is more general than + :class:`pymbolic.mapper.coefficient.CoefficientCollector` as that deals + only with linear expressions, but this collector works for polynomial + expressions too. """ def __init__(self, source_dependent_variables): self.source_dependent_variables = source_dependent_variables From 0994482d89465d36566f2d5345d68f528eb0b739 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:24:44 -0500 Subject: [PATCH 131/209] explain map_product --- pytential/symbolic/pde/reduce_fmms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 0dd829067..98b4dd2f1 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -247,8 +247,10 @@ def map_constant(self, expr): def map_product(self, expr): if len(expr.children) > 2: - left = Product(tuple(expr.children[:2])) - right = Product(tuple(expr.children[2:])) + # rewrite products of more than two children as a nested + # product and recurse to make it easier to handle. + left = expr.children[0] + right = Product(tuple(expr.children[1:])) new_prod = Product((left, right)) return self.rec(new_prod) elif len(expr.children) == 1: From 011780423d9b437cbc607cb5d12bafc7d2c676a4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:29:16 -0500 Subject: [PATCH 132/209] rename some variables --- pytential/symbolic/pde/reduce_fmms.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 98b4dd2f1..582ac5400 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -261,9 +261,9 @@ def map_product(self, expr): d_left = self.rec(left) d_right = self.rec(right) d = defaultdict(lambda: 0) - for k_left, v_left in d_left.items(): - for k_right, v_right in d_right.items(): - d[k_left*k_right] += v_left*v_right + for var_left, coeff_left in d_left.items(): + for var_right, coeff_right in d_right.items(): + d[var_left*var_right] += coeff_left*coeff_right return dict(d) def map_quotient(self, expr): @@ -271,11 +271,11 @@ def map_quotient(self, expr): d_den = self.rec(expr.denominator) if len(d_den) > 1: raise ValueError - den_k, den_v = list(d_den.items())[0] + den_var, den_coeff = list(d_den.items())[0] result = {} - for k in d_num.keys(): - result[d_num[k]/den_k] /= den_v + for num_var, num_coeff in d_num.items(): + result[num_var/den_var] = num_coeff/den_coeff return result def map_power(self, expr): @@ -289,8 +289,8 @@ def map_power(self, expr): return d_base if len(d_base) > 1: raise NotImplementedError("powers are not implemented") - (k, v), = d_base.items() - return {k**exp: v**exp} + (var, coeff), = d_base.items() + return {var**exp: coeff**exp} rec = __call__ @@ -313,6 +313,7 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): # }}} + # {{{ factor the matrix def _syzygy_module(m, generators): @@ -382,6 +383,7 @@ def _factor_right(mat, factor_left): # }}} + # {{{ convert factors back into IntGs def _convert_source_poly_to_int_g_derivs(poly, orig_int_g, axis_vars): From e03e23025f878910045e0bb045d8341f59b97b62 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:31:25 -0500 Subject: [PATCH 133/209] use dict comprehension --- pytential/symbolic/pde/reduce_fmms.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 582ac5400..ea66a579b 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -273,10 +273,8 @@ def map_quotient(self, expr): raise ValueError den_var, den_coeff = list(d_den.items())[0] - result = {} - for num_var, num_coeff in d_num.items(): - result[num_var/den_var] = num_coeff/den_coeff - return result + return {num_var/den_var: num_coeff/den_coeff for + num_var, num_coeff in d_num.items()} def map_power(self, expr): d_base = self.rec(expr.base) From 4d94b64aa42e44cbd9c5eaff41852232e8e5710c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 00:32:21 -0500 Subject: [PATCH 134/209] use python argument unpacking --- pytential/symbolic/pde/reduce_fmms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index ea66a579b..6b8d48b49 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -282,7 +282,7 @@ def map_power(self, expr): # d_exponent should look like {1: k} if len(d_exponent) > 1 or 1 not in d_exponent: raise RuntimeError("nonlinear expression") - exp = list(d_exponent.values())[0] + exp, = d_exponent.values() if exp == 1: return d_base if len(d_base) > 1: From 269fe81e798d6c048222baf0423cee65ed60ccee Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 02:09:12 -0500 Subject: [PATCH 135/209] clarify that syzygy is a left nullspace --- pytential/symbolic/pde/reduce_fmms.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 6b8d48b49..a90fd4316 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -314,11 +314,11 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): # {{{ factor the matrix -def _syzygy_module(m, generators): +def _syzygy_module_groebner_basis_mat(m, generators): """Takes as input a module of polynomials with domain :class:`sympy.EX` represented as a matrix and returns the syzygy module as a matrix of polynomials in the same domain. The syzygy module *S* that is returned as a matrix - satisfies S m = 0. + satisfies S m = 0 and is a left nullspace of the matrix. Using :class:`sympy.EX` because that represents the domain with any symbolic element. Usually we need an Integer or Rational domain, but since there can be unrelated symbols like *mu* in the expression, we need to use a symbolic domain. @@ -354,13 +354,14 @@ def _factor_left(mat, axis_vars): with minimum number of columns of L. To get a good factorisation, what we do is first find a matrix such that S M = 0 where S is the syzygy module converted to a matrix. + It can also be referred to as the left nullspace of the matrix. Then, M.T S.T = 0 which implies that M.T is in the space spanned by the syzygy module of S.T and to get M we get the transpose of that. """ - first_syzygy_module = _syzygy_module(mat, axis_vars) - if len(first_syzygy_module) == 0: + syzygy_of_mat = _syzygy_module_groebner_basis_mat(mat, axis_vars) + if len(syzygy_of_mat) == 0: raise ValueError("could not find a factorization") - return _syzygy_module(first_syzygy_module.T, axis_vars).T + return _syzygy_module(syzygy_of_mat.T, axis_vars).T def _factor_right(mat, factor_left): From fd636b2d4403e8764d1f6dddd34b18bffc5d4287 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 02:27:55 -0500 Subject: [PATCH 136/209] fix bad refactor --- pytential/symbolic/pde/reduce_fmms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index a90fd4316..2883d2d0c 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -361,7 +361,7 @@ def _factor_left(mat, axis_vars): syzygy_of_mat = _syzygy_module_groebner_basis_mat(mat, axis_vars) if len(syzygy_of_mat) == 0: raise ValueError("could not find a factorization") - return _syzygy_module(syzygy_of_mat.T, axis_vars).T + return _syzygy_module_groebner_basis_mat(syzygy_of_mat.T, axis_vars).T def _factor_right(mat, factor_left): From 05e25415db632626a29cd46383763275f55ea81c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 20 Sep 2021 02:45:24 -0500 Subject: [PATCH 137/209] Use LUsolve --- pytential/symbolic/pde/reduce_fmms.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 2883d2d0c..14029d129 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -366,19 +366,8 @@ def _factor_left(mat, axis_vars): def _factor_right(mat, factor_left): """Return the right hand side of the factorisation of the matrix""" - ys = sympy.symbols(f"_y{{0:{factor_left.shape[1]}}}") - factor_right = [] - for i in range(mat.shape[1]): - aug_mat = sympy.zeros(factor_left.shape[0], factor_left.shape[1] + 1) - aug_mat[:, :factor_left.shape[1]] = factor_left - aug_mat[:, factor_left.shape[1]] = mat[:, i] - res_map = sympy.solve_linear_system(aug_mat, *ys) - row = [] - for y in ys: - row.append(res_map[y]) - factor_right.append(row) - factor_right = sympy.Matrix(factor_right).T - return factor_right + return factor_left.LUsolve(sympy.Matrix(mat), + iszerofunc=lambda x: x.simplify() == 0) # }}} From 9ea9b3370e818b103895c9938535f31d13e5aba4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 12:57:32 -0500 Subject: [PATCH 138/209] explain that we are working in a polynomial ring --- pytential/symbolic/pde/reduce_fmms.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 14029d129..660c8b576 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -351,7 +351,11 @@ def _convert_to_matrix(module, *generators): def _factor_left(mat, axis_vars): """Return the left hand side of the factorisation of the matrix For a matrix M, we want to find a factorisation such that M = L R - with minimum number of columns of L. + with minimum number of columns of L. The polynomials represent + derivative operators and therefore division is not well defined. + To avoid divisions, we work in a polynomial ring which doesn't + have division either. + To get a good factorisation, what we do is first find a matrix such that S M = 0 where S is the syzygy module converted to a matrix. It can also be referred to as the left nullspace of the matrix. From 75dea2bb2eca1b803ffcf76e0fed71c884ae7645 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 12:59:49 -0500 Subject: [PATCH 139/209] Explain why we are using LUsolve --- pytential/symbolic/pde/reduce_fmms.py | 8 +++++++- pytential/symbolic/pde/system_utils.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 660c8b576..9af5aaa75 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -369,7 +369,13 @@ def _factor_left(mat, axis_vars): def _factor_right(mat, factor_left): - """Return the right hand side of the factorisation of the matrix""" + """Return the right hand side of the factorisation of the matrix + Note that from the construction of *factor_left*, we know that + the factor on the right has elements in the same polynomial ring + as the input matrix *mat*. Therefore, doing divisions are fine + as they should result in exact polynomial divisions and will have + no remainders. + """ return factor_left.LUsolve(sympy.Matrix(mat), iszerofunc=lambda x: x.simplify() == 0) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index ab8ac71f1..d2e95bae6 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -56,17 +56,22 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, Several techniques are used for merging and reducing number of FMMs * When `base_kernel` is given an `IntG` is rewritten using `base_kernel` - and its derivatives. + and its derivatives. (For example, if *base_kernel* is the biharmonic + kernel, and a Laplace kernel is encountered, this will (forcibly) + rewrite the kernel in terms of that. The routine will fail if this + process cannot be completed.) * :class:`sumpy.kernel.AxisTargetDerivative` instances are converted to :class:`sumpy.kernel.AxisSourceDerivative` instances. + (by flipping signs, assuming translation-invariance). + Target derivatives will be brought back by the syzygy module + construction below if beneficial. + (For example, D + d/dx(S) can be re-written as D - d/dy(S) which can be + done in one FMM) * If there is a sum of two `IntG`s with same target derivative and different source derivatives of the same kernel, they are merged into one FMM. - * If possible, convert :class:`sumpy.kernel.AxisSourceDerivative` to - :class:`sumpy.kernel.DirectionalSourceDerivative`. - * Reduce the number of FMMs by converting the `IntG` expression to a matrix and factoring the matrix where the left operand matrix represents a transformation at target and the right matrix represents a transformation From 0d0082de4540ae6c0041a2ddc639d5e24f324350 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 13:19:09 -0500 Subject: [PATCH 140/209] Add reduce_fmms to private docs --- doc/private.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/private.rst diff --git a/doc/private.rst b/doc/private.rst new file mode 100644 index 000000000..c84fae8ad --- /dev/null +++ b/doc/private.rst @@ -0,0 +1,14 @@ +:orphan: + +Internal APIs +============= + +These are private API that are documented for developers +and should not be used by end users. + +Reduce Number of FMMs +--------------------- + +.. automodule:: pytential.symbolic.pde.reduce_fmms + + From c4b569fa16b212767515c9e91c0d9a80d9481cb8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 12:59:49 -0500 Subject: [PATCH 141/209] Improve docs - Explain why we are using LUsolve - Explain converting target deriv to source deriv and add example --- pytential/symbolic/pde/reduce_fmms.py | 8 +++++++- pytential/symbolic/pde/system_utils.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 660c8b576..9af5aaa75 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -369,7 +369,13 @@ def _factor_left(mat, axis_vars): def _factor_right(mat, factor_left): - """Return the right hand side of the factorisation of the matrix""" + """Return the right hand side of the factorisation of the matrix + Note that from the construction of *factor_left*, we know that + the factor on the right has elements in the same polynomial ring + as the input matrix *mat*. Therefore, doing divisions are fine + as they should result in exact polynomial divisions and will have + no remainders. + """ return factor_left.LUsolve(sympy.Matrix(mat), iszerofunc=lambda x: x.simplify() == 0) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index ab8ac71f1..d2e95bae6 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -56,17 +56,22 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, Several techniques are used for merging and reducing number of FMMs * When `base_kernel` is given an `IntG` is rewritten using `base_kernel` - and its derivatives. + and its derivatives. (For example, if *base_kernel* is the biharmonic + kernel, and a Laplace kernel is encountered, this will (forcibly) + rewrite the kernel in terms of that. The routine will fail if this + process cannot be completed.) * :class:`sumpy.kernel.AxisTargetDerivative` instances are converted to :class:`sumpy.kernel.AxisSourceDerivative` instances. + (by flipping signs, assuming translation-invariance). + Target derivatives will be brought back by the syzygy module + construction below if beneficial. + (For example, D + d/dx(S) can be re-written as D - d/dy(S) which can be + done in one FMM) * If there is a sum of two `IntG`s with same target derivative and different source derivatives of the same kernel, they are merged into one FMM. - * If possible, convert :class:`sumpy.kernel.AxisSourceDerivative` to - :class:`sumpy.kernel.DirectionalSourceDerivative`. - * Reduce the number of FMMs by converting the `IntG` expression to a matrix and factoring the matrix where the left operand matrix represents a transformation at target and the right matrix represents a transformation From 8e2d31aa45b3e35e832b0822acc72b4a44e14926 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 13:19:09 -0500 Subject: [PATCH 142/209] Add reduce_fmms to private docs --- doc/private.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/private.rst diff --git a/doc/private.rst b/doc/private.rst new file mode 100644 index 000000000..c84fae8ad --- /dev/null +++ b/doc/private.rst @@ -0,0 +1,14 @@ +:orphan: + +Internal APIs +============= + +These are private API that are documented for developers +and should not be used by end users. + +Reduce Number of FMMs +--------------------- + +.. automodule:: pytential.symbolic.pde.reduce_fmms + + From bb246dbd6c68f2e58094c8b5bf40174eaef9b734 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 13:38:53 -0500 Subject: [PATCH 143/209] Add system utils to docs and fix doc warnings --- doc/index.rst | 1 + doc/merge.rst | 4 ++++ pytential/symbolic/pde/reduce_fmms.py | 6 +++++- pytential/symbolic/pde/system_utils.py | 16 +++++++++++----- 4 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 doc/merge.rst diff --git a/doc/index.rst b/doc/index.rst index 598cd4641..cdfa3cc26 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -23,6 +23,7 @@ Contents tools misc qbx + merge 🚀 Github 💾 Download Releases diff --git a/doc/merge.rst b/doc/merge.rst new file mode 100644 index 000000000..2c4b15ea2 --- /dev/null +++ b/doc/merge.rst @@ -0,0 +1,4 @@ +Utilities to merge and reduce IntG expressions +============================================== + +.. automodule:: pytential.symbolic.pde.system_utils diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 9af5aaa75..5859f5b06 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -35,6 +35,10 @@ "reduce_number_of_fmms", ) +__doc__=""" +.. autofunction:: reduce_number_of_fmms +""" + # {{{ Reduce number of FMMs - main routine @@ -58,7 +62,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): :arg int_gs: list of ``IntG`` objects. - :arg source_dependent_variables: list of :class:`pymbolic.primtives.Expression` + :arg source_dependent_variables: list of :class:`pymbolic.primitives.Expression` objects. When reducing FMMs, consider only these variables as dependent on source. For eg: densities, source derivative vectors. diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index d2e95bae6..2a8f87560 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -46,6 +46,11 @@ "get_deriv_relation", ) +__doc__ = """ +.. autofunction:: merge_int_g_exprs +.. autofunction:: get_deriv_relation +""" + def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, source_dependent_variables=None): @@ -55,7 +60,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, Several techniques are used for merging and reducing number of FMMs - * When `base_kernel` is given an `IntG` is rewritten using `base_kernel` + * When *base_kernel* is given an *IntG* is rewritten using *base_kernel* and its derivatives. (For example, if *base_kernel* is the biharmonic kernel, and a Laplace kernel is encountered, this will (forcibly) rewrite the kernel in terms of that. The routine will fail if this @@ -66,18 +71,19 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, (by flipping signs, assuming translation-invariance). Target derivatives will be brought back by the syzygy module construction below if beneficial. - (For example, D + d/dx(S) can be re-written as D - d/dy(S) which can be + (For example, `D + d/dx(S)` can be re-written as `D - d/dy(S)` which can be done in one FMM) - * If there is a sum of two `IntG`s with same target derivative and different + * If there is a sum of two *IntG* s with same target derivative and different source derivatives of the same kernel, they are merged into one FMM. - * Reduce the number of FMMs by converting the `IntG` expression to + * Reduce the number of FMMs by converting the *IntG* expression to a matrix and factoring the matrix where the left operand matrix represents a transformation at target and the right matrix represents a transformation at source. For this to work, we need to know which variables depend on source so that they do not end up in the left operand. User needs to supply - this as the argument `source_dependent_variable`. + this as the argument *source_dependent_variable*. This is done by the + call to :func:`pytential.symbolic.pde.reduce_fmms.reduce_number_of_fmms`. :arg base_kernel: A :class:`sumpy.kernel.Kernel` object if given will be used for converting a :class:`~pytential.symbolic.primitives.IntG` to a linear From 5d198f07d44c35ae6a9d71f60ad391e4432de522 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 14:05:20 -0500 Subject: [PATCH 144/209] Use a logger --- pytential/symbolic/pde/system_utils.py | 48 +++++++++++--------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 2a8f87560..c18f78b00 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -41,6 +41,9 @@ from pytential.symbolic.pde.reduce_fmms import reduce_number_of_fmms +import logging +logger = logging.getLogger(__name__) + __all__ = ( "merge_int_g_exprs", "get_deriv_relation", @@ -51,9 +54,7 @@ .. autofunction:: get_deriv_relation """ - -def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, - source_dependent_variables=None): +def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): """ Merge expressions involving :class:`~pytential.symbolic.primitives.IntG` objects. @@ -90,8 +91,6 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, expression of same type with the kernel replaced by base_kernel and its derivatives - :arg verbose: increase verbosity of merging - :arg source_dependent_variable: When merging expressions, consider only these variables as dependent on source. Otherwise consider all variables as source dependent. This is important when reducing the number of FMMs @@ -114,7 +113,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, verbose=False, # Convert IntGs with different kernels to expressions containing # IntGs with base_kernel or its derivatives replacements[int_g] = sum(convert_int_g_to_base(new_int_g, - base_kernel, verbose=verbose) for new_int_g in new_int_g_s) + base_kernel) for new_int_g in new_int_g_s) # replace the IntGs in the expressions substitutor = IntGSubstitutor(replacements) @@ -352,7 +351,7 @@ def evalf(expr, prec=100): @memoize_on_first_arg -def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): +def _get_base_kernel_matrix(base_kernel, order=None): dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() @@ -370,8 +369,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): if order == pde.order: pde_mis = [ident.mi for eq in pde.eqs for ident in eq.keys()] pde_mis = [mi for mi in pde_mis if sum(mi) == order] - if verbose: - print(f"Removing {pde_mis[-1]} to avoid linear dependent mis") + logger.debug(f"Removing {pde_mis[-1]} to avoid linear dependent mis") mis.remove(pde_mis[-1]) rand = np.random.randint(1, 100, (dim, len(mis))) @@ -402,10 +400,8 @@ def _get_base_kernel_matrix(base_kernel, order=None, verbose=False): @memoize_on_first_arg -def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, - verbose=False): - (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, - verbose=verbose) +def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None): + (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order) dim = base_kernel.dim sym_vec = make_sym_vector("d", dim) sympy_conv = SympyToPymbolicMapper() @@ -418,8 +414,7 @@ def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, vec = sym.Matrix(vec) result = [] const = 0 - if verbose: - print(kernel, end=" = ", flush=True) + logger.debug("%s = ", kernel) sol = lu_solve_with_expand(L, U, perm, vec) for i, coeff in enumerate(sol): @@ -430,19 +425,17 @@ def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None, coeff *= kernel.get_global_scaling_const() coeff /= base_kernel.get_global_scaling_const() result.append((mis[i], sympy_conv(coeff))) - if verbose: - print(f"{base_kernel}.diff({mis[i]})*{coeff}", end=" + ") + logger.debug(" + %s.diff(%s)*%s", base_kernel, mis[i], coeff) else: const = sympy_conv(coeff * kernel.get_global_scaling_const()) - if verbose: - print(const) + logger.debug(" + %s", const) return (const, result) -def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, verbose=False): +def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None): res = [] for knl in kernels: - res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order, verbose)) + res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order)) return res @@ -495,16 +488,16 @@ def filter_kernel_arguments(knls, kernel_arguments): return {k: v for (k, v) in kernel_arguments.items() if k in kernel_arg_names} -def convert_int_g_to_base(int_g, base_kernel, verbose=False): +def convert_int_g_to_base(int_g, base_kernel): result = 0 for knl, density in zip(int_g.source_kernels, int_g.densities): result += _convert_int_g_to_base( int_g.copy(source_kernels=(knl,), densities=(density,)), - base_kernel, verbose) + base_kernel) return result -def _convert_int_g_to_base(int_g, base_kernel, verbose=False): +def _convert_int_g_to_base(int_g, base_kernel): tgt_knl = int_g.target_kernel dim = tgt_knl.dim if tgt_knl != tgt_knl.get_base_kernel(): @@ -516,7 +509,7 @@ def _convert_int_g_to_base(int_g, base_kernel, verbose=False): source_kernel = int_g.source_kernels[0] deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), - base_kernel, verbose=verbose) + base_kernel) const = deriv_relation[0] # NOTE: we set a dofdesc here to force the evaluation of this integral @@ -725,6 +718,7 @@ def simplify_densities(densities): if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) from sumpy.kernel import (StokesletKernel, BiharmonicKernel, # noqa:F401 StressletKernel, ElasticityKernel, LaplaceKernel) base_kernel = BiharmonicKernel(3) @@ -742,7 +736,7 @@ def simplify_densities(densities): kernels = [expression_knl, expression_knl2] #kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), # ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] - get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, verbose=True) + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4) density = pytential.sym.make_sym_vector("d", 1)[0] from pytential.symbolic.primitives import int_g_vec int_g_1 = int_g_vec(TargetPointMultiplier(2, AxisTargetDerivative(2, @@ -752,4 +746,4 @@ def simplify_densities(densities): AxisSourceDerivative(0, AxisSourceDerivative(0, LaplaceKernel(3))))), density, qbx_forced_limit=1) print(merge_int_g_exprs([int_g_1, int_g_2], - base_kernel=BiharmonicKernel(3), verbose=True)[0]) + base_kernel=BiharmonicKernel(3))[0]) From 3c05eab36d862c95e72730f65d19a88d15afa9ab Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 14:11:30 -0500 Subject: [PATCH 145/209] Fix comment about source dependent variables to reflect code --- pytential/symbolic/pde/system_utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index c18f78b00..a32fd3236 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -92,9 +92,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): derivatives :arg source_dependent_variable: When merging expressions, consider only these - variables as dependent on source. Otherwise consider all variables - as source dependent. This is important when reducing the number of FMMs - needed for the output. + variables as dependent on source. This is important when reducing the number of + FMMs needed for the output. """ int_g_s = get_int_g_s(exprs) From ef0497dd16146342254f8c6ec5eb0be2f2e9703d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 15:04:16 -0500 Subject: [PATCH 146/209] Use one pass substitutor --- pytential/symbolic/pde/system_utils.py | 59 ++++++++++++-------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index a32fd3236..bb379a70e 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -96,31 +96,13 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): FMMs needed for the output. """ - int_g_s = get_int_g_s(exprs) - if base_kernel is not None: - replacements = {} - - # Iterate all the IntGs in the expressions and create a dictionary - # of replacements for the IntGs - for int_g in int_g_s: - # First convert IntGs with target derivatives to source derivatives - new_int_g = convert_target_deriv_to_source(int_g) - # Convert IntGs with TargetMultiplier to a sum of IntGs without - # TargetMultipliers - new_int_g_s = convert_target_multiplier_to_source(new_int_g) - # Convert IntGs with different kernels to expressions containing - # IntGs with base_kernel or its derivatives - replacements[int_g] = sum(convert_int_g_to_base(new_int_g, - base_kernel) for new_int_g in new_int_g_s) - - # replace the IntGs in the expressions - substitutor = IntGSubstitutor(replacements) - exprs = [substitutor(expr) for expr in exprs] + mapper = RewriteUsingBaseKernelMapper() + exprs = [mapper(expr) for expr in exprs] differing_kernel_arguments = set() kernel_arguments = {} - for int_g in int_g_s: + for int_g in get_int_g_s(exprs): for k, v in int_g.kernel_arguments.items(): if k not in kernel_arguments: kernel_arguments[k] = v @@ -251,6 +233,30 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): return result +class RewriteUsingBaseKernelMapper(IdentityMapper): + """Rewrites IntGs using the base kernel. First this method replaces + IntGs with :class:`sumpy.kernel.AxisTargetDerivative` to IntGs + :class:`sumpy.kernel.AxisSourceDerivative` and then replaces + IntGs with :class:`sumpy.kernel.TargetPointMultiplier` to IntGs + without them using :class:`sumpy.kernel.ExpressionKernel` + and then finally converts them to the base kernel by finding + a relationship between the derivatives. + """ + def __init__(self, base_kernel): + self.base_kernel = base_kernel + + def map_int_g(self, expr): + # First convert IntGs with target derivatives to source derivatives + expr = convert_target_deriv_to_source(expr) + # Convert IntGs with TargetMultiplier to a sum of IntGs without + # TargetMultipliers + new_int_gs = convert_target_multiplier_to_source(expr) + # Convert IntGs with different kernels to expressions containing + # IntGs with base_kernel or its derivatives + return sum(convert_int_g_to_base(new_int_g, + self.base_kernel) for new_int_g in new_int_g_s) + + def get_int_g_source_group(int_g, differing_kernel_arguments): """Return a group for the *int_g* with so that all elements in that group have the same source attributes. @@ -312,17 +318,6 @@ def merge_two_int_gs(int_g_1, int_g_2): ) -class IntGSubstitutor(IdentityMapper): - """Replaces IntGs with pymbolic expression given by the - replacements dictionary - """ - def __init__(self, replacements): - self.replacements = replacements - - def map_int_g(self, expr): - return self.replacements.get(expr, expr) - - class IntGCoefficientCollector(CoefficientCollector): def __init__(self): super().__init__({}) From b32e4ff2a03c607dc68b6c4262c4a2e545956ffa Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 17:43:34 -0500 Subject: [PATCH 147/209] Allow converting IntGs with more than one source_kernels --- pytential/symbolic/pde/system_utils.py | 61 ++++++++++++++------------ 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index bb379a70e..241f80d72 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -492,41 +492,44 @@ def convert_int_g_to_base(int_g, base_kernel): def _convert_int_g_to_base(int_g, base_kernel): - tgt_knl = int_g.target_kernel + target_kernel = int_g.target_kernel dim = tgt_knl.dim - if tgt_knl != tgt_knl.get_base_kernel(): - return int_g - - if not len(int_g.densities) == 1: - raise ValueError - density = int_g.densities[0] - source_kernel = int_g.source_kernels[0] - - deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), - base_kernel) - - const = deriv_relation[0] - # NOTE: we set a dofdesc here to force the evaluation of this integral - # on the source instead of the target when using automatic tagging - # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` - dd = pytential.sym.DOFDescriptor(None, - discr_stage=pytential.sym.QBX_SOURCE_STAGE1) - const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) result = 0 - if source_kernel == source_kernel.get_base_kernel(): + for density, source_kernel in zip(int_g.densities, int_g.source_kernels): + deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), + base_kernel) + + const = deriv_relation[0] + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = pytential.sym.DOFDescriptor(None, + discr_stage=pytential.sym.QBX_SOURCE_STAGE1) + const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) + + if const != 0 and target_kernel != target_kernel.get_base_kernel(): + # There might be some TargetPointMultipliers hanging around. + # FIXME: handle them instead of bailing out + return [int_g] + + if source_kernel != source_kernel.get_base_kernel(): + # We assume that any source transformation is a derivative + # and the constant when applied becomes zero. + const = 0 + result += const - new_kernel_args = filter_kernel_arguments([base_kernel], int_g.kernel_arguments) + new_kernel_args = filter_kernel_arguments([base_kernel], int_g.kernel_arguments) - for mi, c in deriv_relation[1]: - knl = source_kernel.replace_base_kernel(base_kernel) - for d, val in enumerate(mi): - for _ in range(val): - knl = AxisSourceDerivative(d, knl) - c *= -1 - result += int_g.copy(target_kernel=base_kernel, source_kernels=(knl,), - densities=(density,), kernel_arguments=new_kernel_args) * c + for mi, c in deriv_relation[1]: + knl = source_kernel.replace_base_kernel(base_kernel) + for d, val in enumerate(mi): + for _ in range(val): + knl = AxisSourceDerivative(d, knl) + c *= -1 + result += int_g.copy(source_kernels=(knl,), + densities=(density,), kernel_arguments=new_kernel_args) * c return result From 90fd5b048a4bd6f8095f63b88b5952a114a97db4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 20:03:13 -0500 Subject: [PATCH 148/209] make normal vector names unique --- pytential/symbolic/pde/system_utils.py | 116 +++++++++++++++++++------ 1 file changed, 89 insertions(+), 27 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 241f80d72..be0824573 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -100,18 +100,11 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): mapper = RewriteUsingBaseKernelMapper() exprs = [mapper(expr) for expr in exprs] - differing_kernel_arguments = set() - kernel_arguments = {} - for int_g in get_int_g_s(exprs): - for k, v in int_g.kernel_arguments.items(): - if k not in kernel_arguments: - kernel_arguments[k] = v - else: - if kernel_arguments[k] != v: - differing_kernel_arguments.add(k) + from sumpy.assignment_collection import SymbolicAssignmentCollection + seen_normal_vectors = SymbolicAssignmentCollection() # Using a dictionary instead of a set because sets are unordered - all_source_groups = dict() + all_source_group_identifiers = dict() result = np.array([0 for _ in exprs], dtype=object) @@ -145,12 +138,13 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # move the coefficient inside int_g = int_g.copy(densities=[density*coeff for density in int_g.densities]) + int_g = make_normal_vector_names_unique(int_g, seen_normal_vectors) - source_group = get_int_g_source_group(int_g, differing_kernel_arguments) + source_group_identifier = get_int_g_source_group_identifier(int_g, differing_kernel_arguments) target_group = get_int_g_target_group(int_g) - group = (source_group, target_group) + group = (source_group_identifier, target_group) - all_source_groups[source_group] = 1 + all_source_group_identifiers[source_group_identifier] = 1 if group not in int_gs_by_group: new_int_g = int_g @@ -183,12 +177,12 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): result[iexpr] += int_g return result - # Do the calculation for each source_group separately and assemble them - for source_group in all_source_groups.keys(): + # Do the calculation for each source_group_identifier separately and assemble them + for source_group_identifier in all_source_group_identifiers.keys(): insn_to_idx_mapping = defaultdict(list) for idx, int_gs_by_group in enumerate(int_gs_by_group_for_index): for group, int_g in int_gs_by_group.items(): - if group[0] != source_group: + if group[0] != source_group_identifier: continue # For each output, we now have a sum of int_gs with # different target attributes. @@ -257,18 +251,85 @@ def map_int_g(self, expr): self.base_kernel) for new_int_g in new_int_g_s) -def get_int_g_source_group(int_g, differing_kernel_arguments): +def make_normal_vector_names_unique(int_g, sac): + normal_vectors = get_normal_vectors(intg) + replacements = {} + for vector_name, value in normal_vectors.items(): + if vector_name not in sac.assignments: + # There was no previous IntG with the same normal vector name + sac.assignments[vector_name] = value + sac.reversed_assignments[value] = vector_name + elif sac.assignments[vector_name] != value: + # There was a previous IntG with the same normal vector name + # and the value was different. We need to rename + new_name = sac.symbol_generator(vector_name).name + # If this name was already renamed, use that name + if value in sac.reversed_assignments: + new_name = sac.reversed_assignments[value] + else: + sac.assignments[new_name] = value + sac.reversed_assignments[value] = new_name + replacements[name] = new_name + + target_kernel = rename_normal_vector_name(int_g.target_kernel, + replacements) + source_kernels = tuple([rename_normal_vector_name(source_kernel, + replacements) for source_kernel in int_g.source_kernels]) + + kernel_arguments = int_g.kernel_arguments.copy() + # first delete the old names and then add in the new names + # these have to be done in two loops to avoid issues with + # some new names conflicting with old names + for old_name in replacements.keys(): + del kernel_arguments[old_name] + for old_name, new_name in replacements.items(): + kernel_arguments[new_name] = int_g.kernel_arguments[old_name] + + return int_g.copy(target_kernel=target_kernel, source_kernels=source_kernels) + + +def rename_normal_vector_name(kernel, replacements): + if not isinstance(kernel, KernelWrapper): + return kernel + renamed_inner_kernel = rename_normal_vector_name(kernel.inner_kernel, + old_name, new_name) + + if not isinstance(kernel, DirectionalDerivative): + return kernel.replace_inner_kernel(renamed_inner_kernel) + + new_name = replacements[kernel.dir_vec_name] + return type(kernel)(renamed_inner_kernel, new_name) + + +def get_normal_vectors(int_g): + """Return the normal vector names and their values from a + *int_g*. + """ + normal_vectors = {} + kernels = [int_g.target_kernel] + list(int_g.source_kernels) + for kernel in kernels: + while isinstance(kernel, KernelWrapper): + if isinstance(kernel, DirectionalDerivative): + name = kernel.dir_vec_name + normal_vectors[name] = get_hashable_kernel_argument( + int_g.kernel_arguments[name]) + kernel = kernel.inner_kernel + return normal_vectors + + +def get_hashable_kernel_argument(arg): + if hasattr(arg, "__iter__"): + return tuple(arg) + return arg + + +def get_int_g_source_group_identifier(int_g): """Return a group for the *int_g* with so that all elements in that group have the same source attributes. """ - def get_hashable(arg): - if hasattr(arg, "__iter__"): - return tuple(arg) - return arg - - args = tuple((k, get_hashable(v)) for k, v in sorted( - int_g.kernel_arguments.items()) if k in - differing_kernel_arguments) + normal_vectors = get_normal_vectors(int_g) + args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( + int_g.kernel_arguments.items()) if k not in normal_vectors) return (int_g.source, args, int_g.target_kernel.get_base_kernel()) @@ -493,7 +554,7 @@ def convert_int_g_to_base(int_g, base_kernel): def _convert_int_g_to_base(int_g, base_kernel): target_kernel = int_g.target_kernel - dim = tgt_knl.dim + dim = target_kernel.dim result = 0 for density, source_kernel in zip(int_g.densities, int_g.source_kernels): @@ -520,7 +581,8 @@ def _convert_int_g_to_base(int_g, base_kernel): result += const - new_kernel_args = filter_kernel_arguments([base_kernel], int_g.kernel_arguments) + new_kernel_args = filter_kernel_arguments([base_kernel], + int_g.kernel_arguments) for mi, c in deriv_relation[1]: knl = source_kernel.replace_base_kernel(base_kernel) From d030a8ada27b5cef856b443cc5bb0158d32f3c14 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 20:07:33 -0500 Subject: [PATCH 149/209] Fix flake8 and bring back IntGSubstitutor --- pytential/symbolic/pde/reduce_fmms.py | 2 +- pytential/symbolic/pde/system_utils.py | 31 ++++++++++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 5859f5b06..4037c1091 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -35,7 +35,7 @@ "reduce_number_of_fmms", ) -__doc__=""" +__doc__ = """ .. autofunction:: reduce_number_of_fmms """ diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index be0824573..7068a8882 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -25,7 +25,7 @@ from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, DirectionalSourceDerivative, ExpressionKernel, - KernelWrapper, TargetPointMultiplier) + KernelWrapper, TargetPointMultiplier, DirectionalDerivative) from pytools import (memoize_on_first_arg, generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) @@ -54,6 +54,7 @@ .. autofunction:: get_deriv_relation """ + def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): """ Merge expressions involving :class:`~pytential.symbolic.primitives.IntG` @@ -92,8 +93,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): derivatives :arg source_dependent_variable: When merging expressions, consider only these - variables as dependent on source. This is important when reducing the number of - FMMs needed for the output. + variables as dependent on source. This is important when reducing the + number of FMMs needed for the output. """ if base_kernel is not None: @@ -140,7 +141,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): int_g.densities]) int_g = make_normal_vector_names_unique(int_g, seen_normal_vectors) - source_group_identifier = get_int_g_source_group_identifier(int_g, differing_kernel_arguments) + source_group_identifier = get_int_g_source_group_identifier(int_g) target_group = get_int_g_target_group(int_g) group = (source_group_identifier, target_group) @@ -177,7 +178,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): result[iexpr] += int_g return result - # Do the calculation for each source_group_identifier separately and assemble them + # Do the calculation for each source_group_identifier separately + # and assemble them for source_group_identifier in all_source_group_identifiers.keys(): insn_to_idx_mapping = defaultdict(list) for idx, int_gs_by_group in enumerate(int_gs_by_group_for_index): @@ -248,11 +250,11 @@ def map_int_g(self, expr): # Convert IntGs with different kernels to expressions containing # IntGs with base_kernel or its derivatives return sum(convert_int_g_to_base(new_int_g, - self.base_kernel) for new_int_g in new_int_g_s) + self.base_kernel) for new_int_g in new_int_gs) def make_normal_vector_names_unique(int_g, sac): - normal_vectors = get_normal_vectors(intg) + normal_vectors = get_normal_vectors(int_g) replacements = {} for vector_name, value in normal_vectors.items(): if vector_name not in sac.assignments: @@ -269,7 +271,7 @@ def make_normal_vector_names_unique(int_g, sac): else: sac.assignments[new_name] = value sac.reversed_assignments[value] = new_name - replacements[name] = new_name + replacements[vector_name] = new_name target_kernel = rename_normal_vector_name(int_g.target_kernel, replacements) @@ -292,7 +294,7 @@ def rename_normal_vector_name(kernel, replacements): if not isinstance(kernel, KernelWrapper): return kernel renamed_inner_kernel = rename_normal_vector_name(kernel.inner_kernel, - old_name, new_name) + replacements) if not isinstance(kernel, DirectionalDerivative): return kernel.replace_inner_kernel(renamed_inner_kernel) @@ -340,6 +342,17 @@ def get_int_g_target_group(int_g): return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel) +class IntGSubstitutor(IdentityMapper): + """Replaces IntGs with pymbolic expression given by the + replacements dictionary + """ + def __init__(self, replacements): + self.replacements = replacements + + def map_int_g(self, expr): + return self.replacements.get(expr, expr) + + def remove_target_attributes(int_g): """Remove target attributes from *int_g* and return an expression that is common to all expression in the same source group. From bed059813d6aff1006563af9a996d0c0564c0f46 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Sep 2021 20:10:05 -0500 Subject: [PATCH 150/209] insn_to_idx_mapping -> targetless_int_g_to_idx_mapping --- pytential/symbolic/pde/system_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 7068a8882..3f833d84d 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -181,7 +181,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # Do the calculation for each source_group_identifier separately # and assemble them for source_group_identifier in all_source_group_identifiers.keys(): - insn_to_idx_mapping = defaultdict(list) + targetless_int_g_to_idx_mapping = defaultdict(list) for idx, int_gs_by_group in enumerate(int_gs_by_group_for_index): for group, int_g in int_gs_by_group.items(): if group[0] != source_group_identifier: @@ -217,14 +217,14 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # [S + D, S, D]. We will reduce these and restore the target # attributes at the end common_int_g = remove_target_attributes(int_g) - insn_to_idx_mapping[common_int_g].append((idx, int_g)) + targetless_int_g_to_idx_mapping[common_int_g].append((idx, int_g)) - insns_to_reduce = list(insn_to_idx_mapping.keys()) + insns_to_reduce = list(targetless_int_g_to_idx_mapping.keys()) reduced_insns = reduce_number_of_fmms(insns_to_reduce, source_dependent_variables) for insn, reduced_insn in zip(insns_to_reduce, reduced_insns): - for idx, int_g in insn_to_idx_mapping[insn]: + for idx, int_g in targetless_int_g_to_idx_mapping[insn]: result[idx] += restore_target_attributes(reduced_insn, int_g) return result From 2723c15de5c23bd665301de1ff0678a054222fd9 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 27 Sep 2021 00:51:54 -0500 Subject: [PATCH 151/209] Fix passing base_kernel --- pytential/symbolic/pde/system_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 3f833d84d..472e8130a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -98,7 +98,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): """ if base_kernel is not None: - mapper = RewriteUsingBaseKernelMapper() + mapper = RewriteUsingBaseKernelMapper(base_kernel) exprs = [mapper(expr) for expr in exprs] from sumpy.assignment_collection import SymbolicAssignmentCollection From a3c35f97765a70b4b3b360b305457888a51e1565 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 27 Sep 2021 10:57:36 -0500 Subject: [PATCH 152/209] Fix making normal vector names unique --- pytential/symbolic/pde/system_utils.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 472e8130a..f93ea7729 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -128,6 +128,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): result[i] += coeff continue + int_g = make_normal_vector_names_unique(int_g, seen_normal_vectors) # convert DirectionalSourceDerivative to AxisSourceDerivative # as kernel arguments need to be the same for merging if source_dependent_variables is not None: @@ -139,11 +140,11 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # move the coefficient inside int_g = int_g.copy(densities=[density*coeff for density in int_g.densities]) - int_g = make_normal_vector_names_unique(int_g, seen_normal_vectors) - source_group_identifier = get_int_g_source_group_identifier(int_g) - target_group = get_int_g_target_group(int_g) - group = (source_group_identifier, target_group) + source_group_identifier = get_int_g_source_group_identifier(int_g, + seen_normal_vectors.assignments) + target_group_identifier = get_int_g_target_group_identifier(int_g) + group = (source_group_identifier, target_group_identifier) all_source_group_identifiers[source_group_identifier] = 1 @@ -299,7 +300,7 @@ def rename_normal_vector_name(kernel, replacements): if not isinstance(kernel, DirectionalDerivative): return kernel.replace_inner_kernel(renamed_inner_kernel) - new_name = replacements[kernel.dir_vec_name] + new_name = replacements.get(kernel.dir_vec_name, kernel.dir_vec_name) return type(kernel)(renamed_inner_kernel, new_name) @@ -325,17 +326,16 @@ def get_hashable_kernel_argument(arg): return arg -def get_int_g_source_group_identifier(int_g): +def get_int_g_source_group_identifier(int_g, normal_vectors): """Return a group for the *int_g* with so that all elements in that group have the same source attributes. """ - normal_vectors = get_normal_vectors(int_g) args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( int_g.kernel_arguments.items()) if k not in normal_vectors) return (int_g.source, args, int_g.target_kernel.get_base_kernel()) -def get_int_g_target_group(int_g): +def get_int_g_target_group_identifier(int_g): """Return a group for the *int_g* with so that all elements in that group have the same source attributes. """ @@ -566,7 +566,7 @@ def convert_int_g_to_base(int_g, base_kernel): def _convert_int_g_to_base(int_g, base_kernel): - target_kernel = int_g.target_kernel + target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) dim = target_kernel.dim result = 0 @@ -603,7 +603,7 @@ def _convert_int_g_to_base(int_g, base_kernel): for _ in range(val): knl = AxisSourceDerivative(d, knl) c *= -1 - result += int_g.copy(source_kernels=(knl,), + result += int_g.copy(source_kernels=(knl,), target_kernel=target_kernel, densities=(density,), kernel_arguments=new_kernel_args) * c return result From 3d50ac8eabcf4c7279e785de2af7945cb250c92f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 27 Sep 2021 12:29:14 -0500 Subject: [PATCH 153/209] Use {} instead of dict() --- pytential/symbolic/pde/system_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index f93ea7729..1453581c1 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -105,7 +105,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): seen_normal_vectors = SymbolicAssignmentCollection() # Using a dictionary instead of a set because sets are unordered - all_source_group_identifiers = dict() + all_source_group_identifiers = {} result = np.array([0 for _ in exprs], dtype=object) From 9133bda1ec1b49908a859c1b5051a15ace997d25 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:42:47 -0500 Subject: [PATCH 154/209] pymbolic has __iter__ that throws --- pytential/symbolic/pde/system_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 1453581c1..a1075587b 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -322,7 +322,10 @@ def get_normal_vectors(int_g): def get_hashable_kernel_argument(arg): if hasattr(arg, "__iter__"): - return tuple(arg) + try: + return tuple(arg) + except TypeError: + pass return arg From 896e2d15a15c105e443cf190cd833550f9c78cfd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:43:50 -0500 Subject: [PATCH 155/209] Add a comment about folding duplicates in IntG --- pytential/symbolic/primitives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 7374858eb..5e046f9c5 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1431,13 +1431,13 @@ def __init__(self, target_kernel, source_kernels, densities, raise ValueError("invalid value (%s) of qbx_forced_limit" % qbx_forced_limit) + # Fold duplicates in source_kernels knl_density_dict = OrderedDict() for density, source_kernel in zip(densities, source_kernels): if source_kernel in knl_density_dict: knl_density_dict[source_kernel] += density else: knl_density_dict[source_kernel] = density - knl_density_dict = OrderedDict((k, v) for k, v in knl_density_dict.items() if v) densities = tuple(knl_density_dict.values()) From d547d7d67c88930e7c7bef549ac9993a2ac77c25 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:46:27 -0500 Subject: [PATCH 156/209] document extra_deriv_dirs --- pytential/symbolic/stokes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 7d00cefa3..494515088 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -86,6 +86,8 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): :arg density_vec_sym: a symbolic vector variable for the density vector. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. + :arg extra_deriv_dirs: adds target derivatives to all the integral + objects with the given derivative axis. """ raise NotImplementedError @@ -185,6 +187,8 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, :arg dir_vec_sym: a symbolic vector variable for the direction vector. :arg qbx_forced_limit: the *qbx_forced_limit* argument to be passed on to :class:`~pytential.symbolic.primitives.IntG`. + :arg extra_deriv_dirs: adds target derivatives to all the integral + objects with the given derivative axis. """ raise NotImplementedError From f948fc3531693e34c83be807a5382099ae0c34b5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:47:46 -0500 Subject: [PATCH 157/209] Use ABC for base classes --- pytential/symbolic/stokes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 494515088..e4baae61e 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -28,6 +28,7 @@ ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from pymbolic import var +from abc import ABC __doc__ = """ .. autoclass:: StokesletWrapper @@ -44,7 +45,7 @@ _MU_SYM_DEFAULT = var("mu") -class StokesletWrapperBase: +class StokesletWrapperBase(ABC): """Wrapper class for the :class:`~sumpy.kernel.StokesletKernel` kernel. This class is meant to shield the user from the messiness of writing @@ -144,7 +145,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): raise NotImplementedError -class StressletWrapperBase: +class StressletWrapperBase(ABC): """Wrapper class for the :class:`~sumpy.kernel.StressletKernel` kernel. This class is meant to shield the user from the messiness of writing From 585bd344ea94a3ed2e34e24b6e155fa0fabaaf54 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:49:14 -0500 Subject: [PATCH 158/209] note that apply_pressure doesn't depend on implementation --- pytential/symbolic/stokes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index e4baae61e..7301deef6 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -94,6 +94,9 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): def apply_pressure(self, density_vec_sym, qbx_forced_limit): """Symbolic expression for pressure field associated with the Stokeslet.""" + # Pressure representation doesn't differ depending on the implementation + # and is implemented in base class here. + from pytential.symbolic.mappers import DerivativeTaker kernel = LaplaceKernel(dim=self.dim) sym_expr = 0 @@ -194,7 +197,11 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, raise NotImplementedError def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): - """Symbolic expression for pressure field associated with the Stresslet.""" + """Symbolic expression for pressure field associated with the Stresslet. + """ + # Pressure representation doesn't differ depending on the implementation + # and is implemented in base class here. + import itertools from pytential.symbolic.mappers import DerivativeTaker kernel = LaplaceKernel(dim=self.dim) From 8d43a958783f6abb534435390b97c7536d6f9b5c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:50:50 -0500 Subject: [PATCH 159/209] S -> int_g_vec as it's not a single layer kernel --- pytential/symbolic/stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 7301deef6..cfbdab0c4 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -267,7 +267,7 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): for arg in args: kwargs[arg] = var(arg) - res = sym.S(knl, density, **kwargs) + res = sym.int_g_vec(knl, density, **kwargs) return res From 85a9e4f0564862ce85e30c0b9e0833ddad3ad0b5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:54:16 -0500 Subject: [PATCH 160/209] move stokeslet to the top --- pytential/symbolic/stokes.py | 55 ++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index cfbdab0c4..2d466b8e1 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -480,6 +480,33 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # {{{ Stokeslet/Stresslet using Laplace +class StokesletWrapperTornberg(StokesletWrapperBase): + """A Stresslet wrapper using Tornberg and Greengard's method which + uses Laplace derivatives. + + [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the + three-dimensional Stokes equations. + Journal of Computational Physics, 227(3), 1613-1619. + """ + + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + self.dim = dim + if dim != 3: + raise ValueError("unsupported dimension given to " + "StokesletWrapperTornberg") + if nu_sym != 0.5: + raise ValueError("nu != 0.5 is not supported") + self.kernel = LaplaceKernel(dim=self.dim) + self.mu = mu_sym + self.nu = nu_sym + + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): + stresslet = StressletWrapperTornberg(3, self.mu, self.nu) + return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, + [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, + extra_deriv_dirs) + + class StressletWrapperTornberg(StressletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -567,34 +594,6 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, return sym_expr - -class StokesletWrapperTornberg(StokesletWrapperBase): - """A Stresslet wrapper using Tornberg and Greengard's method which - uses Laplace derivatives. - - [1] Tornberg, A. K., & Greengard, L. (2008). A fast multipole method for the - three-dimensional Stokes equations. - Journal of Computational Physics, 227(3), 1613-1619. - """ - - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - self.dim = dim - if dim != 3: - raise ValueError("unsupported dimension given to " - "StokesletWrapperTornberg") - if nu_sym != 0.5: - raise ValueError("nu != 0.5 is not supported") - self.kernel = LaplaceKernel(dim=self.dim) - self.mu = mu_sym - self.nu = nu_sym - - def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): - stresslet = StressletWrapperTornberg(3, self.mu, self.nu) - return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, - [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, - extra_deriv_dirs) - - # }}} From 6075c8898069ddd3579c46d686b164ba645913cf Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 16:54:53 -0500 Subject: [PATCH 161/209] update vim section comment --- pytential/symbolic/stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 2d466b8e1..c4ce1aca3 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -478,7 +478,7 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # }}} -# {{{ Stokeslet/Stresslet using Laplace +# {{{ Stokeslet/Stresslet using Laplace (Tornberg) class StokesletWrapperTornberg(StokesletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which From 1bd32328742eace9aecf743c3f66eee8bd7841f8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 18:03:15 -0500 Subject: [PATCH 162/209] refactor base classes --- pytential/symbolic/stokes.py | 80 ++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index c4ce1aca3..9b1fbc403 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -40,7 +40,7 @@ """ -# {{{ StokesletWrapper +# {{{ StokesletWrapper/StressletWrapper ABCs _MU_SYM_DEFAULT = var("mu") @@ -258,6 +258,10 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, """ raise NotImplementedError +# }}} + + +# {{{ StokesletWrapper/StressletWrapper Naive and Biharmonic impl def _create_int_g(knl, deriv_dirs, density, **kwargs): for deriv_dir in deriv_dirs: @@ -271,7 +275,7 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): return res -class StokesletWrapper(StokesletWrapperBase): +class _StokesletWrapperNaiveOrBiharmonic(StokesletWrapperBase): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method="biharmonic"): super().__init__(dim, mu_sym, nu_sym) @@ -343,12 +347,25 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): return sym_expr + +class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + method="naive") + + +class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + method="biharmonic") + + # }}} # {{{ StressletWrapper -class StressletWrapper(StressletWrapperBase): +class _StressletWrapperNaiveOrBiharmonic(StressletWrapperBase): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method="biharmonic"): super().__init__(dim, mu_sym, nu_sym) @@ -358,8 +375,12 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, self.method = method if method == "biharmonic": self.base_kernel = BiharmonicKernel(dim) + self.stokeslet_obj = StokesletWrapperBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu) elif method == "naive": self.base_kernel = None + self.stokeslet_obj = StokesletWrapperNaive(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu) else: raise ValueError("method has to be one of biharmonic/naive") @@ -434,15 +455,12 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, extra_deriv_dirs=()): - stokeslet_obj = StokesletWrapper(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu, method=self.method) - sym_expr = 0 if stresslet_weight != 0: sym_expr += self.apply(stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stresslet_weight if stokeslet_weight != 0: - sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, + sym_expr += self.stokeslet_obj.apply(stokeslet_density_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight return merge_int_g_exprs(sym_expr) @@ -475,6 +493,18 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, return sym_expr + +class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + method="naive") + + +class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + method="biharmonic") + # }}} @@ -597,6 +627,42 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, # }}} +class StokesletWrapper(StokesletWrapperBase): + def __new__(dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): + if method == None: + import warnings + warnings.warn("method argument not given. falling back to 'naive'" + "method argument will be required in the future.") + method = "naive" + if method == "naive": + return StokesletWrapperNaive(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "biharmonic": + return StokesletWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "laplace": + return StokesletWrapperTornberg(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + else: + raise ValueError(f"invalid method: {method}." + "Needs to be one of naive, laplace, biharmonic") + + +class StressletWrapper(StressletWrapperBase): + def __new__(dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): + if method == None: + import warnings + warnings.warn("method argument not given. falling back to 'naive'" + "method argument will be required in the future.") + method = "naive" + if method == "naive": + return StressletWrapperNaive(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "biharmonic": + return StressletWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "laplace": + return StressletWrapperTornberg(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + else: + raise ValueError(f"invalid method: {method}." + "Needs to be one of naive, laplace, biharmonic") + + # {{{ base Stokes operator class StokesOperator: From 302c5fb13cc890e0fe5781e9a0818882e74d7e52 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 18:16:17 -0500 Subject: [PATCH 163/209] simplify logic --- pytential/symbolic/stokes.py | 54 +++++++++++++++++------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 9b1fbc403..44a2cf766 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -261,7 +261,7 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # }}} -# {{{ StokesletWrapper/StressletWrapper Naive and Biharmonic impl +# {{{ StokesletWrapper Naive and Biharmonic impl def _create_int_g(knl, deriv_dirs, density, **kwargs): for deriv_dir in deriv_dirs: @@ -363,7 +363,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): # }}} -# {{{ StressletWrapper +# {{{ StressletWrapper Naive and Biharmonic impl class _StressletWrapperNaiveOrBiharmonic(StressletWrapperBase): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, @@ -627,9 +627,11 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, # }}} +# {{{ StokesletWrapper dispatch class + class StokesletWrapper(StokesletWrapperBase): - def __new__(dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): - if method == None: + def __new__(cls, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): + if method is None: import warnings warnings.warn("method argument not given. falling back to 'naive'" "method argument will be required in the future.") @@ -639,15 +641,21 @@ def __new__(dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): elif method == "biharmonic": return StokesletWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) elif method == "laplace": - return StokesletWrapperTornberg(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + if nu_sym == 0.5: + return StokesletWrapperTornberg(dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) + else: + from pytential.symbolic.elasticity import StokesletWrapperYoshida + return StokesletWrapperYoshida(dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") class StressletWrapper(StressletWrapperBase): - def __new__(dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): - if method == None: + def __new__(cls, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): + if method is None: import warnings warnings.warn("method argument not given. falling back to 'naive'" "method argument will be required in the future.") @@ -657,11 +665,19 @@ def __new__(dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): elif method == "biharmonic": return StressletWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) elif method == "laplace": - return StressletWrapperTornberg(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + if nu_sym == 0.5: + return StressletWrapperTornberg(dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) + else: + from pytential.symbolic.elasticity import StressletWrapperYoshida + return StressletWrapperYoshida(dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") +# }}} + # {{{ base Stokes operator @@ -684,9 +700,6 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. """ - from pytential.symbolic.elasticity import (StressletWrapperYoshida, - StokesletWrapperYoshida) - if side not in [+1, -1]: raise ValueError(f"invalid evaluation side: {side}") @@ -695,25 +708,10 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym - if method == "laplace": - if nu_sym == 0.5: - self.stresslet = StressletWrapperTornberg(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapperTornberg(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) - else: - self.stresslet = StressletWrapperYoshida(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) - self.stokeslet = StokesletWrapperYoshida(dim=self.ambient_dim, - mu_sym=mu_sym, nu_sym=nu_sym) - elif method == "biharmonic" or method == "naive": - self.stresslet = StressletWrapper(dim=self.ambient_dim, + self.stresslet = StressletWrapper(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym, method=method) - self.stokeslet = StokesletWrapper(dim=self.ambient_dim, + self.stokeslet = StokesletWrapper(dim=self.ambient_dim, mu_sym=mu_sym, nu_sym=nu_sym, method=method) - else: - raise ValueError(f"invalid method: {method}." - "Needs to be one of naive, laplace, biharmonic") @property def dim(self): From 61b718099a1ee8c21c25b5ff7ea2fe5c79d6fb68 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 23:16:11 -0500 Subject: [PATCH 164/209] fix calling __init__ --- pytential/symbolic/stokes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 44a2cf766..0a7f86dee 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -350,13 +350,13 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="naive") class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="biharmonic") @@ -496,13 +496,13 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="naive") class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="biharmonic") # }}} From d899cdd435b6a4df14ea91d6858f3dc7081f142c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 23:16:36 -0500 Subject: [PATCH 165/209] fix passing nu and method --- test/test_stokes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index d7cecf7f3..b45e5ad9f 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -545,8 +545,8 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): actx_factory, case, identity, resolution=resolution, visualize=visualize, - nu=nu, - method=method) + nu=0.5, + method='naive') for eoc, e in zip(eocs, errors): eoc.add_data_point(h_max, e) From e14b3d4821086a9714834122386042d80f806403 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 23:20:16 -0500 Subject: [PATCH 166/209] use double quotes --- test/test_stokes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index b45e5ad9f..8b611c2df 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -546,7 +546,7 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): resolution=resolution, visualize=visualize, nu=0.5, - method='naive') + method="naive") for eoc, e in zip(eocs, errors): eoc.add_data_point(h_max, e) From f7cdf142f319343f4aaae2e5f69d2b34adbc4ffc Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 29 Sep 2021 23:36:31 -0500 Subject: [PATCH 167/209] Fix lints from pylint --- pytential/symbolic/stokes.py | 16 +++++++++------- test/test_stokes.py | 9 +++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 0a7f86dee..1002ad66e 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -285,8 +285,12 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, self.method = method if method == "biharmonic": self.base_kernel = BiharmonicKernel(dim) + self.stresslet_obj = StressletWrapperBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu) elif method == "naive": self.base_kernel = None + self.stresslet_obj = StressletWrapperBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu) else: raise ValueError("method has to be one of biharmonic/naive") @@ -332,8 +336,6 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) - stresslet_obj = StressletWrapper(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu, method=self.method) # For stokeslet, there's no direction vector involved # passing a list of ones instead to remove its usage. @@ -341,7 +343,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += dir_vec_sym[i] * \ - stresslet_obj.get_int_g((comp, i, j), + self.stresslet_obj.get_int_g((comp, i, j), density_vec_sym[j], [1]*self.dim, qbx_forced_limit, deriv_dirs=[]) @@ -350,13 +352,13 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="naive") class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="biharmonic") @@ -496,13 +498,13 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="naive") class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - return super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, method="biharmonic") # }}} diff --git a/test/test_stokes.py b/test/test_stokes.py index 8b611c2df..c58099d7a 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -457,8 +457,7 @@ def __init__(self, ambient_dim): def apply_operator(self): sym_density = sym.normal(self.ambient_dim).as_vector() return self.stokeslet.apply( - sym_density, - mu_sym=1, qbx_forced_limit=+1) + sym_density, qbx_forced_limit=+1) def ref_result(self): return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) @@ -517,7 +516,7 @@ def apply_operator(self): sym_density = sym.normal(self.ambient_dim).as_vector() return self.stokeslet.apply_stress( sym_density, sym_density, - mu_sym=1, qbx_forced_limit="avg") + qbx_forced_limit="avg") def ref_result(self): return -0.5 * sym.normal(self.ambient_dim).as_vector() @@ -544,9 +543,7 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): h_max, errors = run_stokes_identity( actx_factory, case, identity, resolution=resolution, - visualize=visualize, - nu=0.5, - method="naive") + visualize=visualize) for eoc, e in zip(eocs, errors): eoc.add_data_point(h_max, e) From 24aedd8325240a5c0e7632f69b36095511b6e0c2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 30 Sep 2021 01:19:48 -0500 Subject: [PATCH 168/209] revert setting stresslet obj --- pytential/symbolic/stokes.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 1002ad66e..f0d44cba5 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -285,12 +285,8 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, self.method = method if method == "biharmonic": self.base_kernel = BiharmonicKernel(dim) - self.stresslet_obj = StressletWrapperBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu) elif method == "naive": self.base_kernel = None - self.stresslet_obj = StressletWrapperBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu) else: raise ValueError("method has to be one of biharmonic/naive") @@ -336,6 +332,8 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) + stresslet_obj = StressletWrapper(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu, method=self.method) # For stokeslet, there's no direction vector involved # passing a list of ones instead to remove its usage. @@ -343,7 +341,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): for i in range(self.dim): for j in range(self.dim): sym_expr[comp] += dir_vec_sym[i] * \ - self.stresslet_obj.get_int_g((comp, i, j), + stresslet_obj.get_int_g((comp, i, j), density_vec_sym[j], [1]*self.dim, qbx_forced_limit, deriv_dirs=[]) @@ -377,12 +375,8 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, self.method = method if method == "biharmonic": self.base_kernel = BiharmonicKernel(dim) - self.stokeslet_obj = StokesletWrapperBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu) elif method == "naive": self.base_kernel = None - self.stokeslet_obj = StokesletWrapperNaive(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu) else: raise ValueError("method has to be one of biharmonic/naive") @@ -457,12 +451,15 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, extra_deriv_dirs=()): + stokeslet_obj = StokesletWrapper(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu, method=self.method) + sym_expr = 0 if stresslet_weight != 0: sym_expr += self.apply(stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stresslet_weight if stokeslet_weight != 0: - sym_expr += self.stokeslet_obj.apply(stokeslet_density_vec_sym, + sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight return merge_int_g_exprs(sym_expr) From 0c03e0a75db790006054f63b3ccc0d51ba94102b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 30 Sep 2021 05:01:52 -0500 Subject: [PATCH 169/209] Fix bad merge --- test/test_stokes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_stokes.py b/test/test_stokes.py index c58099d7a..acbfde9a4 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -335,6 +335,8 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) qbx_order=qbx_order, source_ovsmp=source_ovsmp, resolution=resolution, + method=method, + nu=nu, visualize=visualize) for eoc, e in zip(eocs, errors): From fb13205f215fc37d3721fe4d18ed14901f0f9843 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 30 Sep 2021 17:35:53 -0500 Subject: [PATCH 170/209] fix processing source[i] in densities --- pytential/symbolic/pde/reduce_fmms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 4037c1091..39d7c240b 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -239,6 +239,9 @@ def map_algebraic_leaf(self, expr): else: return {1: expr} + def map_node_coordinate_component(self, expr): + return {expr: 1} + def map_subscript(self, expr): if expr in self.source_dependent_variables or \ expr.aggregate in self.source_dependent_variables: From 1b59ffcb4617ec8befeca3b4ae555c96b3e64995 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 30 Sep 2021 17:38:23 -0500 Subject: [PATCH 171/209] Fix restoring target attributes --- pytential/symbolic/pde/system_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index a1075587b..3f6627cc7 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -374,8 +374,8 @@ def restore_target_attributes(expr, orig_int_g): replacements = { int_g: int_g.copy(target=orig_int_g.target, qbx_forced_limit=orig_int_g.qbx_forced_limit, - target_kernel=int_g.target_kernel.replace_base_kernel( - orig_int_g.target_kernel)) + target_kernel=orig_int_g.target_kernel.replace_base_kernel( + int_g.target_kernel)) for int_g in int_gs} substitutor = IntGSubstitutor(replacements) From 4561c26a6d2ba77fb62c77208ddb68138d7bd333 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 30 Sep 2021 17:38:36 -0500 Subject: [PATCH 172/209] Add test for restoring target attributes --- test/test_pde_system_utils.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index d3ae209d4..a54b1ef78 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -239,6 +239,24 @@ def test_merge_directional_source(): assert result[0] == int_g4 +def test_restoring_target_attributes(): + from pymbolic.primitives import Variable + dim = 3 + laplace_knl = LaplaceKernel(dim) + density = Variable("density") + + int_g1 = int_g_vec(TargetPointMultiplier(0, AxisTargetDerivative(0, + laplace_knl)), density, qbx_forced_limit=1) + int_g2 = int_g_vec(AxisTargetDerivative(1, laplace_knl), + density, qbx_forced_limit=1) + + result = merge_int_g_exprs([int_g1, int_g2], + source_dependent_variables=[]) + + assert result[0] == int_g1 + assert result[1] == int_g2 + + # You can test individual routines by typing # $ python test_pde_system_tools.py 'test_reduce_number_of_fmms()' From 26fa31b846161fafa0e553ff534dcf271b9d434a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 1 Oct 2021 14:20:59 -0500 Subject: [PATCH 173/209] Fix another bad merge with sending mu --- pytential/symbolic/stokes.py | 2 +- test/test_stokes.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index f0d44cba5..ec7ee3a35 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -386,7 +386,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, for j in range(i, dim): for k in range(j, dim): self.kernel_dict[(i, j, k)] = StressletKernel(dim=dim, icomp=i, - jcomp=j, kcomp=k) + jcomp=j, kcomp=k, viscosity_mu=mu_sym) # The dictionary allows us to exploit symmetry -- that # :math:`T_{012}` is identical to :math:`T_{120}` -- and avoid creating diff --git a/test/test_stokes.py b/test/test_stokes.py index acbfde9a4..1db756bca 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -454,12 +454,13 @@ class StokesletIdentity: def __init__(self, ambient_dim): from pytential.symbolic.stokes import StokesletWrapper self.ambient_dim = ambient_dim - self.stokeslet = StokesletWrapper(self.ambient_dim) + self.stokeslet = StokesletWrapper(self.ambient_dim, mu_sym=1) def apply_operator(self): sym_density = sym.normal(self.ambient_dim).as_vector() return self.stokeslet.apply( - sym_density, qbx_forced_limit=+1) + sym_density, + qbx_forced_limit=+1) def ref_result(self): return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) @@ -512,7 +513,7 @@ class StressletIdentity: def __init__(self, ambient_dim): from pytential.symbolic.stokes import StokesletWrapper self.ambient_dim = ambient_dim - self.stokeslet = StokesletWrapper(self.ambient_dim) + self.stokeslet = StokesletWrapper(self.ambient_dim, mu_sym=1) def apply_operator(self): sym_density = sym.normal(self.ambient_dim).as_vector() From 78287b6e0407a9c822b850013cb529795672aa49 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 3 Oct 2021 16:29:26 -0500 Subject: [PATCH 174/209] add some debug logging --- pytential/symbolic/pde/reduce_fmms.py | 5 +++++ pytential/symbolic/pde/system_utils.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 39d7c240b..4cabab23f 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -30,6 +30,9 @@ import functools from collections import defaultdict +import logging +logger = logging.getLogger(__name__) + __all__ = ( "reduce_number_of_fmms", @@ -84,6 +87,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): mat, source_exprs = _create_matrix(int_gs, source_dependent_variables, axis_vars) except ValueError: + logger.debug("could not create matrix from %s", int_gs) return int_gs mat = sympy.Matrix(mat) @@ -92,6 +96,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): left_factor = _factor_left(mat, axis_vars) right_factor = _factor_right(mat, left_factor) except ValueError: + logger.debug("could not find a factorization for %s", mat) return int_gs # If there are n inputs and m outputs, diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 3f6627cc7..4db8d1c57 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -120,6 +120,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # FIXME: if there's ever any use case, then we can extract # some IntGs from them. result[i] += expr + logger.debug("%s is not linear", expr) continue int_gs_by_group = {} for int_g, coeff in int_g_coeff_map.items(): @@ -788,6 +789,7 @@ def simplify_densities(densities): try: result.append(to_pymbolic(to_sympy(density))) except (ValueError, NotImplementedError, UnsupportedExpressionError): + logger.debug("%s cannot be simplified", density) result.append(density) return tuple(result) From e3391d8975f45c170e7b1d8fdb4e9a90f206a396 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 3 Oct 2021 16:29:42 -0500 Subject: [PATCH 175/209] Fix when there are non linear expressions --- pytential/symbolic/pde/system_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 4db8d1c57..a20ff4f66 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -121,6 +121,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # some IntGs from them. result[i] += expr logger.debug("%s is not linear", expr) + int_gs_by_group_for_index.append({}) continue int_gs_by_group = {} for int_g, coeff in int_g_coeff_map.items(): From 2c1ca38261b3a3357556fc1f2c15f80061ce5be8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 3 Oct 2021 16:30:04 -0500 Subject: [PATCH 176/209] don't check with np.array --- pytential/symbolic/pde/system_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index a20ff4f66..c2db68190 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -770,7 +770,8 @@ def merge_kernel_arguments(x, y): res = x.copy() for k, v in y.items(): if k in res: - if not res[k] == v: + if get_hashable_kernel_argument(res[k]) \ + != get_hashable_kernel_argument(v): raise ValueError else: res[k] = v From 33376f1975a2c5ee931e0b6a8407b093130a29cd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 7 Oct 2021 15:16:58 -0500 Subject: [PATCH 177/209] Increase randomint range and retry if that also fails --- pytential/symbolic/pde/system_utils.py | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index c2db68190..3d29436b5 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -22,7 +22,8 @@ import numpy as np -from sumpy.symbolic import make_sym_vector, sym, SympyToPymbolicMapper +from sumpy.symbolic import make_sym_vector, SympyToPymbolicMapper +import sumpy.symbolic as sym from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, DirectionalSourceDerivative, ExpressionKernel, KernelWrapper, TargetPointMultiplier, DirectionalDerivative) @@ -424,7 +425,7 @@ def evalf(expr, prec=100): @memoize_on_first_arg -def _get_base_kernel_matrix(base_kernel, order=None): +def _get_base_kernel_matrix(base_kernel, order=None, retries=3): dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() @@ -445,13 +446,15 @@ def _get_base_kernel_matrix(base_kernel, order=None): logger.debug(f"Removing {pde_mis[-1]} to avoid linear dependent mis") mis.remove(pde_mis[-1]) - rand = np.random.randint(1, 100, (dim, len(mis))) + rand = np.random.randint(1, 10**15, (dim, len(mis))) sym_vec = make_sym_vector("d", dim) base_expr = base_kernel.get_expression(sym_vec) mat = [] for rand_vec_idx in range(rand.shape[1]): + rand_int = rand[:, rand_vec_idx] + rand_rat = [sym.sympify(i)/10**15 for i in rand_int] row = [] for mi in mis[:-1]: expr = base_expr @@ -460,7 +463,7 @@ def _get_base_kernel_matrix(base_kernel, order=None): continue expr = expr.diff(sym_vec[var_idx], nderivs) replace_dict = dict( - (k, v) for k, v in zip(sym_vec, rand[:, rand_vec_idx]) + (k, v) for k, v in zip(sym_vec, rand_rat) ) eval_expr = evalf(expr.xreplace(replace_dict)) row.append(eval_expr) @@ -468,7 +471,23 @@ def _get_base_kernel_matrix(base_kernel, order=None): mat.append(row) mat = sym.Matrix(mat) - L, U, perm = mat.LUdecomposition() + failed = False + try: + L, U, perm = mat.LUdecomposition() + except RuntimeError: + # symengine throws an error when rank deficient + # and sympy returns U with last row zero + failed = True + + if not sym.USE_SYMENGINE and all(expr == 0 for expr in U[-1, :]): + failed = True + + if failed: + if retries == 0: + raise RuntimeError("Failed to find a base kernel") + return _get_base_kernel_matrix(base_kernel, order=order, + retries=retries - 1) + return (L, U, perm), rand, mis From 423c02950bc79cf6e825b82388d441c00212b560 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 7 Oct 2021 15:17:29 -0500 Subject: [PATCH 178/209] Allow target dependent variables --- pytential/symbolic/pde/system_utils.py | 56 +++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 3d29436b5..1b7dbcb61 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -32,7 +32,8 @@ as gnitstam) from collections import defaultdict -from pymbolic.mapper import WalkMapper +from pymbolic.geometric_algebra.mapper import WalkMapper +from pymbolic.mapper import CombineMapper from pymbolic.mapper.coefficient import CoefficientCollector from pymbolic.primitives import Variable from pytential.symbolic.primitives import IntG, NodeCoordinateComponent @@ -140,14 +141,16 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # convert TargetDerivative to source before checking the group # as the target kernel has to be the same for merging int_g = convert_target_deriv_to_source(int_g) - # move the coefficient inside - int_g = int_g.copy(densities=[density*coeff for density in + if not is_expr_target_dependent(coeff): + # move the coefficient inside + int_g = int_g.copy(densities=[density*coeff for density in int_g.densities]) + coeff = 1 source_group_identifier = get_int_g_source_group_identifier(int_g, seen_normal_vectors.assignments) target_group_identifier = get_int_g_target_group_identifier(int_g) - group = (source_group_identifier, target_group_identifier) + group = (source_group_identifier, target_group_identifier, coeff) all_source_group_identifiers[source_group_identifier] = 1 @@ -173,13 +176,17 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): int_gs_by_group_for_index.append(int_gs_by_group) + # No IntGs found + if all(not int_gs_by_group for int_gs_by_group in int_gs_by_group_for_index): + return exprs + # If source_dependent_variables (sdv) is not given return early. # Check for (sdv is None) instead of just (sdv) because # it can be an empty list. if source_dependent_variables is None: for iexpr, int_gs_by_group in enumerate(int_gs_by_group_for_index): - for int_g in int_gs_by_group.values(): - result[iexpr] += int_g + for group, int_g in int_gs_by_group.items(): + result[iexpr] += group[2] * int_g return result # Do the calculation for each source_group_identifier separately @@ -221,18 +228,49 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # [S + D, S, D]. We will reduce these and restore the target # attributes at the end common_int_g = remove_target_attributes(int_g) - targetless_int_g_to_idx_mapping[common_int_g].append((idx, int_g)) + targetless_int_g_to_idx_mapping[common_int_g].append((idx, int_g, group[2])) insns_to_reduce = list(targetless_int_g_to_idx_mapping.keys()) reduced_insns = reduce_number_of_fmms(insns_to_reduce, source_dependent_variables) for insn, reduced_insn in zip(insns_to_reduce, reduced_insns): - for idx, int_g in targetless_int_g_to_idx_mapping[insn]: - result[idx] += restore_target_attributes(reduced_insn, int_g) + for idx, int_g, coeff in targetless_int_g_to_idx_mapping[insn]: + result[idx] += coeff * restore_target_attributes(reduced_insn, int_g) return result +def is_expr_target_dependent(expr): + mapper = IsExprTargetDependent() + return mapper(expr) + + +class IsExprTargetDependent(CombineMapper): + def combine(self, values): + import operator + from functools import reduce + return reduce(operator.or_, values, False) + + def map_constant(self, expr): + return False + + map_variable = map_constant + map_wildcard = map_constant + map_function_symbol = map_constant + + def map_common_subexpression(self, expr): + return self.rec(expr.child) + + def map_coordinate_component(self, expr): + return True + + def map_num_reference_derivative(self, expr): + return True + + def map_q_weight(self, expr): + return True + + class RewriteUsingBaseKernelMapper(IdentityMapper): """Rewrites IntGs using the base kernel. First this method replaces IntGs with :class:`sumpy.kernel.AxisTargetDerivative` to IntGs From 8a2779a0300d3bd8697252bdf4d0bd2b510994a6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 7 Oct 2021 16:35:24 -0500 Subject: [PATCH 179/209] Fix formatting --- pytential/symbolic/pde/system_utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 1b7dbcb61..0ec2015aa 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -185,8 +185,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # it can be an empty list. if source_dependent_variables is None: for iexpr, int_gs_by_group in enumerate(int_gs_by_group_for_index): - for group, int_g in int_gs_by_group.items(): - result[iexpr] += group[2] * int_g + for (_, _, coeff), int_g in int_gs_by_group.items(): + result[iexpr] += coeff * int_g return result # Do the calculation for each source_group_identifier separately @@ -228,7 +228,8 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # [S + D, S, D]. We will reduce these and restore the target # attributes at the end common_int_g = remove_target_attributes(int_g) - targetless_int_g_to_idx_mapping[common_int_g].append((idx, int_g, group[2])) + targetless_int_g_to_idx_mapping[common_int_g].append((idx, int_g, + group[2])) insns_to_reduce = list(targetless_int_g_to_idx_mapping.keys()) reduced_insns = reduce_number_of_fmms(insns_to_reduce, @@ -523,8 +524,11 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3): if failed: if retries == 0: raise RuntimeError("Failed to find a base kernel") - return _get_base_kernel_matrix(base_kernel, order=order, - retries=retries - 1) + return _get_base_kernel_matrix( + base_kernel=base_kernel, + order=order, + retries=retries-1, + ) return (L, U, perm), rand, mis From e89d96f5757a19e70128d806d37d17a9dba48de3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 8 Oct 2021 08:32:54 -0500 Subject: [PATCH 180/209] revert maxwell test changes --- test/test_maxwell.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_maxwell.py b/test/test_maxwell.py index 371adbdf7..50288443c 100644 --- a/test/test_maxwell.py +++ b/test/test_maxwell.py @@ -352,10 +352,7 @@ def eval_inc_field_at(places, source=None, target=None): inc_xyz_sym = EHField(sym.make_sym_vector("inc_fld", 6)) - mfie_j = mfie.j_operator(loc_sign, jt_sym) - from pytential.symbolic.pde.system_utils import merge_int_g_exprs - mfie_j = merge_int_g_exprs(mfie_j) - bound_j_op = bind(places, mfie_j) + bound_j_op = bind(places, mfie.j_operator(loc_sign, jt_sym)) j_rhs = bind(places, mfie.j_rhs(inc_xyz_sym.h))( actx, inc_fld=inc_field_scat.field, **knl_kwargs) From 81d3162211839ac2b3d1a92757d1eea9026862a7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 8 Oct 2021 09:05:05 -0500 Subject: [PATCH 181/209] use retries exclusively --- pytential/symbolic/pde/system_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 0ec2015aa..017cdadda 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -485,15 +485,13 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3): logger.debug(f"Removing {pde_mis[-1]} to avoid linear dependent mis") mis.remove(pde_mis[-1]) - rand = np.random.randint(1, 10**15, (dim, len(mis))) + rand = np.random.randint(1, 100, (dim, len(mis))) sym_vec = make_sym_vector("d", dim) base_expr = base_kernel.get_expression(sym_vec) mat = [] for rand_vec_idx in range(rand.shape[1]): - rand_int = rand[:, rand_vec_idx] - rand_rat = [sym.sympify(i)/10**15 for i in rand_int] row = [] for mi in mis[:-1]: expr = base_expr @@ -502,7 +500,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3): continue expr = expr.diff(sym_vec[var_idx], nderivs) replace_dict = dict( - (k, v) for k, v in zip(sym_vec, rand_rat) + (k, v) for k, v in zip(sym_vec, rand[:, rand_vec_idx]) ) eval_expr = evalf(expr.xreplace(replace_dict)) row.append(eval_expr) From 21a0ab75d5404632e4125eccbbb38380640a5ab0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 8 Oct 2021 09:25:23 -0500 Subject: [PATCH 182/209] Go back to 10**15 and fix rand matrix --- pytential/symbolic/pde/system_utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 017cdadda..f5e929740 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -485,7 +485,11 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3): logger.debug(f"Removing {pde_mis[-1]} to avoid linear dependent mis") mis.remove(pde_mis[-1]) - rand = np.random.randint(1, 100, (dim, len(mis))) + rand = np.random.randint(1, 10**15, (dim, len(mis))) + rand = rand.astype(object) + for i in range(rand.shape[0]): + for j in range(rand.shape[1]): + rand[i, j] = sym.sympify(rand[i, j])/10**15 sym_vec = make_sym_vector("d", dim) base_expr = base_kernel.get_expression(sym_vec) @@ -532,7 +536,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3): @memoize_on_first_arg -def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-8, order=None): +def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None): (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order) dim = base_kernel.dim sym_vec = make_sym_vector("d", dim) From c3dc80e6d1e1f5cac752d861339a31a38249397b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 8 Oct 2021 16:45:49 -0500 Subject: [PATCH 183/209] Support IntGs with IntGs in the densities --- pytential/symbolic/pde/system_utils.py | 207 ++++++++++++++----------- 1 file changed, 116 insertions(+), 91 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index f5e929740..908a8e51e 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -111,21 +111,30 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): result = np.array([0 for _ in exprs], dtype=object) - int_gs_by_group_for_index = [] - int_g_cc = IntGCoefficientCollector() + int_gs_by_source_group = defaultdict(list) + + def add_int_gs_in_expr(expr): + for int_g in get_int_g_s([expr]): + source_group_identifier = get_int_g_source_group_identifier(int_g, + seen_normal_vectors.assignments) + int_gs_by_source_group[source_group_identifier].append(int_g) + for density in int_g.densities: + add_int_gs_in_expr(density) + for i, expr in enumerate(exprs): + int_gs_by_group = {} try: int_g_coeff_map = int_g_cc(expr) except (RuntimeError, AssertionError): # Don't touch this expression, because it's not linear. # FIXME: if there's ever any use case, then we can extract # some IntGs from them. - result[i] += expr logger.debug("%s is not linear", expr) - int_gs_by_group_for_index.append({}) + expr = make_normal_vector_names_unique(expr, seen_normal_vectors) + result[i] += expr + add_int_gs_in_expr(expr) continue - int_gs_by_group = {} for int_g, coeff in int_g_coeff_map.items(): if int_g == 1: # coeff map may have some constant terms, add them to @@ -163,7 +172,7 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): int_gs_by_group[group] = new_int_g # Do some simplifications after merging. Not stricty necessary - for group, int_g in int_gs_by_group.items(): + for (source_group, _, coeff), int_g in int_gs_by_group.items(): # replace an IntG with d axis source derivatives to an IntG # with one directional source derivative # TODO: reenable this later @@ -172,73 +181,69 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): # not doing automatic simplifications unlike sympy/symengine result_int_g = int_g.copy( densities=simplify_densities(int_g.densities)) - int_gs_by_group[group] = result_int_g - - int_gs_by_group_for_index.append(int_gs_by_group) + result[i] += result_int_g * coeff + add_int_gs_in_expr(result_int_g) # No IntGs found - if all(not int_gs_by_group for int_gs_by_group in int_gs_by_group_for_index): + if all(not int_gs for int_gs in int_gs_by_source_group): return exprs # If source_dependent_variables (sdv) is not given return early. # Check for (sdv is None) instead of just (sdv) because # it can be an empty list. if source_dependent_variables is None: - for iexpr, int_gs_by_group in enumerate(int_gs_by_group_for_index): - for (_, _, coeff), int_g in int_gs_by_group.items(): - result[iexpr] += coeff * int_g return result # Do the calculation for each source_group_identifier separately # and assemble them - for source_group_identifier in all_source_group_identifiers.keys(): - targetless_int_g_to_idx_mapping = defaultdict(list) - for idx, int_gs_by_group in enumerate(int_gs_by_group_for_index): - for group, int_g in int_gs_by_group.items(): - if group[0] != source_group_identifier: - continue - # For each output, we now have a sum of int_gs with - # different target attributes. - # for eg: {+}S + {-}D (where {x} is the QBX limit). - # We can't merge them together, because merging implies - # that everything happens at the source level and therefore - # require same target attributes. - # - # To handle this case, we can treat them separately as in - # different source base kernels, but that would imply more - # FMMs than necessary. - # - # Take the following example, - # - # [ {+}(S + D), {-}S + {avg}D, {avg}S + {-}D] - # - # If we treated the target attributes separately, then we - # will be reducing [{+}(S + D), 0, 0], [0, {-}S, {-}D], - # [0, {avg}D, {avg}S] separately which results in - # [{+}(S + D)], [{-}S, {-}D], [{avg}S, {avg}D] as - # the reduced FMMs and pytential will calculate - # [S + D, S, D] as three separate FMMs and then assemble - # the three outputs by applying target attributes. - # - # Instead, we can do S, D as two separate FMMs and get the - # result for all three outputs. To do that, we will first - # get all five expressions in the example - # [ {+}(S + D), {-}S, {avg}D, {avg}S, {-}D] - # and then remove the target attributes to get, - # [S + D, S, D]. We will reduce these and restore the target - # attributes at the end - common_int_g = remove_target_attributes(int_g) - targetless_int_g_to_idx_mapping[common_int_g].append((idx, int_g, - group[2])) - - insns_to_reduce = list(targetless_int_g_to_idx_mapping.keys()) + replacements = {} + for source_group, int_gs in int_gs_by_source_group.items(): + # For each output, we now have a sum of int_gs with + # different target attributes. + # for eg: {+}S + {-}D (where {x} is the QBX limit). + # We can't merge them together, because merging implies + # that everything happens at the source level and therefore + # require same target attributes. + # + # To handle this case, we can treat them separately as in + # different source base kernels, but that would imply more + # FMMs than necessary. + # + # Take the following example, + # + # [ {+}(S + D), {-}S + {avg}D, {avg}S + {-}D] + # + # If we treated the target attributes separately, then we + # will be reducing [{+}(S + D), 0, 0], [0, {-}S, {-}D], + # [0, {avg}D, {avg}S] separately which results in + # [{+}(S + D)], [{-}S, {-}D], [{avg}S, {avg}D] as + # the reduced FMMs and pytential will calculate + # [S + D, S, D] as three separate FMMs and then assemble + # the three outputs by applying target attributes. + # + # Instead, we can do S, D as two separate FMMs and get the + # result for all three outputs. To do that, we will first + # get all five expressions in the example + # [ {+}(S + D), {-}S, {avg}D, {avg}S, {-}D] + # and then remove the target attributes to get, + # [S + D, S, D]. We will reduce these and restore the target + # attributes at the end + + targetless_int_g_mapping = defaultdict(list) + for int_g in int_gs: + common_int_g = remove_target_attributes(int_g) + targetless_int_g_mapping[common_int_g].append(int_g) + + insns_to_reduce = list(targetless_int_g_mapping.keys()) reduced_insns = reduce_number_of_fmms(insns_to_reduce, source_dependent_variables) for insn, reduced_insn in zip(insns_to_reduce, reduced_insns): - for idx, int_g, coeff in targetless_int_g_to_idx_mapping[insn]: - result[idx] += coeff * restore_target_attributes(reduced_insn, int_g) - return result + for int_g in targetless_int_g_mapping[insn]: + replacements[int_g] = restore_target_attributes(reduced_insn, int_g) + + mapper = IntGSubstitutor(replacements) + return [mapper(expr) for expr in result] def is_expr_target_dependent(expr): @@ -296,41 +301,53 @@ def map_int_g(self, expr): self.base_kernel) for new_int_g in new_int_gs) -def make_normal_vector_names_unique(int_g, sac): - normal_vectors = get_normal_vectors(int_g) - replacements = {} - for vector_name, value in normal_vectors.items(): - if vector_name not in sac.assignments: - # There was no previous IntG with the same normal vector name - sac.assignments[vector_name] = value - sac.reversed_assignments[value] = vector_name - elif sac.assignments[vector_name] != value: - # There was a previous IntG with the same normal vector name - # and the value was different. We need to rename - new_name = sac.symbol_generator(vector_name).name - # If this name was already renamed, use that name - if value in sac.reversed_assignments: - new_name = sac.reversed_assignments[value] - else: - sac.assignments[new_name] = value - sac.reversed_assignments[value] = new_name - replacements[vector_name] = new_name - - target_kernel = rename_normal_vector_name(int_g.target_kernel, - replacements) - source_kernels = tuple([rename_normal_vector_name(source_kernel, - replacements) for source_kernel in int_g.source_kernels]) +def make_normal_vector_names_unique(expr, sac): + mapper = MakeNormalVectorNamesUniqueMapper(sac) + return mapper(expr) - kernel_arguments = int_g.kernel_arguments.copy() - # first delete the old names and then add in the new names - # these have to be done in two loops to avoid issues with - # some new names conflicting with old names - for old_name in replacements.keys(): - del kernel_arguments[old_name] - for old_name, new_name in replacements.items(): - kernel_arguments[new_name] = int_g.kernel_arguments[old_name] - return int_g.copy(target_kernel=target_kernel, source_kernels=source_kernels) +class MakeNormalVectorNamesUniqueMapper(IdentityMapper): + def __init__(self, sac): + self.sac = sac + + def map_int_g(self, int_g): + sac = self.sac + normal_vectors = get_normal_vectors(int_g) + replacements = {} + for vector_name, value in normal_vectors.items(): + if vector_name not in sac.assignments: + # There was no previous IntG with the same normal vector name + sac.assignments[vector_name] = value + sac.reversed_assignments[value] = vector_name + elif sac.assignments[vector_name] != value: + # There was a previous IntG with the same normal vector name + # and the value was different. We need to rename + new_name = sac.symbol_generator(vector_name).name + # If this name was already renamed, use that name + if value in sac.reversed_assignments: + new_name = sac.reversed_assignments[value] + else: + sac.assignments[new_name] = value + sac.reversed_assignments[value] = new_name + replacements[vector_name] = new_name + + target_kernel = rename_normal_vector_name(int_g.target_kernel, + replacements) + source_kernels = tuple([rename_normal_vector_name(source_kernel, + replacements) for source_kernel in int_g.source_kernels]) + + kernel_arguments = int_g.kernel_arguments.copy() + # first delete the old names and then add in the new names + # these have to be done in two loops to avoid issues with + # some new names conflicting with old names + for old_name in replacements.keys(): + del kernel_arguments[old_name] + for old_name, new_name in replacements.items(): + kernel_arguments[new_name] = int_g.kernel_arguments[old_name] + + densities = [self.rec(density) for density in int_g.densities] + return int_g.copy(target_kernel=target_kernel, source_kernels=source_kernels, + densities=tuple(densities)) def rename_normal_vector_name(kernel, replacements): @@ -395,7 +412,15 @@ def __init__(self, replacements): self.replacements = replacements def map_int_g(self, expr): - return self.replacements.get(expr, expr) + if expr in self.replacements: + new_expr = self.replacements[expr] + if new_expr != expr: + return self.rec(new_expr) + else: + expr = new_expr + + densities = [self.rec(density) for density in expr.densities] + return expr.copy(densities=tuple(densities)) def remove_target_attributes(int_g): From 38d40183581ebe51baab3c072ecd07b12d63ac32 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 9 Oct 2021 14:11:26 -0500 Subject: [PATCH 184/209] pull out base kernel finder to a new function --- pytential/symbolic/pde/system_utils.py | 728 +++++++++++++------------ pytential/symbolic/stokes.py | 34 +- test/test_pde_system_utils.py | 9 +- 3 files changed, 393 insertions(+), 378 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 908a8e51e..99dd74987 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -52,24 +52,322 @@ ) __doc__ = """ +.. autofunction:: rewrite_using_base_kernel .. autofunction:: merge_int_g_exprs .. autofunction:: get_deriv_relation """ -def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): +# {{{ rewrite_using_base_kernel + +def rewrite_using_base_kernel(exprs, base_kernel): + """Rewrites an expression with :class:`~pytential.symbolic.primitives.IntG` + objects using *base_kernel*. + + For example, if *base_kernel* is the Biharmonic kernel, and a Laplace kernel + is encountered, this will (forcibly) rewrite the Laplace kernel in terms of + derivatives of the Biharmonic kernel. + + The routine will fail if this process cannot be completed. + """ + mapper = RewriteUsingBaseKernelMapper(base_kernel) + return [mapper(expr) for expr in exprs] + + +class RewriteUsingBaseKernelMapper(IdentityMapper): + """Rewrites IntGs using the base kernel. First this method replaces + IntGs with :class:`sumpy.kernel.AxisTargetDerivative` to IntGs + :class:`sumpy.kernel.AxisSourceDerivative` and then replaces + IntGs with :class:`sumpy.kernel.TargetPointMultiplier` to IntGs + without them using :class:`sumpy.kernel.ExpressionKernel` + and then finally converts them to the base kernel by finding + a relationship between the derivatives. + """ + def __init__(self, base_kernel): + self.base_kernel = base_kernel + + def map_int_g(self, expr): + # First convert IntGs with target derivatives to source derivatives + expr = convert_target_deriv_to_source(expr) + # Convert IntGs with TargetMultiplier to a sum of IntGs without + # TargetMultipliers + new_int_gs = convert_target_multiplier_to_source(expr) + # Convert IntGs with different kernels to expressions containing + # IntGs with base_kernel or its derivatives + return sum(convert_int_g_to_base(new_int_g, + self.base_kernel) for new_int_g in new_int_gs) + + +def convert_target_deriv_to_source(int_g): + """Converts AxisTargetDerivatives to AxisSourceDerivative instances + from an IntG. If there are outer TargetPointMultiplier transformations + they are preserved. + """ + knl = int_g.target_kernel + source_kernels = list(int_g.source_kernels) + coeff = 1 + multipliers = [] + while isinstance(knl, TargetPointMultiplier): + multipliers.append(knl.axis) + knl = knl.inner_kernel + + while isinstance(knl, AxisTargetDerivative): + coeff *= -1 + source_kernels = [AxisSourceDerivative(knl.axis, source_knl) for + source_knl in source_kernels] + knl = knl.inner_kernel + + # TargetPointMultiplier has to be the outermost kernel + # If it is the inner kernel, return early + if isinstance(knl, TargetPointMultiplier): + return 1, int_g + + for axis in reversed(multipliers): + knl = TargetPointMultiplier(axis, knl) + + new_densities = tuple(density*coeff for density in int_g.densities) + return int_g.copy(target_kernel=knl, + densities=new_densities, + source_kernels=tuple(source_kernels)) + + +def convert_target_multiplier_to_source(int_g): + """Convert an IntG with TargetMultiplier to an sum of IntGs without + TargetMultiplier and only source dependent transformations + """ + from sumpy.symbolic import SympyToPymbolicMapper + tgt_knl = int_g.target_kernel + if not isinstance(tgt_knl, TargetPointMultiplier): + return [int_g] + if isinstance(tgt_knl.inner_kernel, KernelWrapper): + return [int_g] + + new_kernel_args = filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) + result = [] + # If the kernel is G, source is y and target is x, + # x G = y*G + (x - y)*G + # For y*G, absorb y into a density + new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) + for density in int_g.densities] + result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, + densities=tuple(new_densities), kernel_arguments=new_kernel_args)) + + # create a new expression kernel for (x - y)*G + sym_d = make_sym_vector("d", tgt_knl.dim) + conv = SympyToPymbolicMapper() + + for knl, density in zip(int_g.source_kernels, int_g.densities): + new_expr = conv(knl.postprocess_at_source(knl.get_expression(sym_d), sym_d) + * sym_d[tgt_knl.axis]) + new_knl = ExpressionKernel(knl.dim, new_expr, + knl.get_base_kernel().global_scaling_const, + knl.is_complex_valued) + result.append(int_g.copy(target_kernel=new_knl, + densities=(density,), + source_kernels=(new_knl,), kernel_arguments=new_kernel_args)) + return result + + +def convert_int_g_to_base(int_g, base_kernel): + result = 0 + for knl, density in zip(int_g.source_kernels, int_g.densities): + result += _convert_int_g_to_base( + int_g.copy(source_kernels=(knl,), densities=(density,)), + base_kernel) + return result + + +def _convert_int_g_to_base(int_g, base_kernel): + target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) + dim = target_kernel.dim + + result = 0 + for density, source_kernel in zip(int_g.densities, int_g.source_kernels): + deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), + base_kernel) + + const = deriv_relation[0] + # NOTE: we set a dofdesc here to force the evaluation of this integral + # on the source instead of the target when using automatic tagging + # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` + dd = pytential.sym.DOFDescriptor(None, + discr_stage=pytential.sym.QBX_SOURCE_STAGE1) + const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) + + if const != 0 and target_kernel != target_kernel.get_base_kernel(): + # There might be some TargetPointMultipliers hanging around. + # FIXME: handle them instead of bailing out + return [int_g] + + if source_kernel != source_kernel.get_base_kernel(): + # We assume that any source transformation is a derivative + # and the constant when applied becomes zero. + const = 0 + + result += const + + new_kernel_args = filter_kernel_arguments([base_kernel], + int_g.kernel_arguments) + + for mi, c in deriv_relation[1]: + knl = source_kernel.replace_base_kernel(base_kernel) + for d, val in enumerate(mi): + for _ in range(val): + knl = AxisSourceDerivative(d, knl) + c *= -1 + result += int_g.copy(source_kernels=(knl,), target_kernel=target_kernel, + densities=(density,), kernel_arguments=new_kernel_args) * c + return result + + +def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None): + res = [] + for knl in kernels: + res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order)) + return res + + +@memoize_on_first_arg +def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None): + (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order) + dim = base_kernel.dim + sym_vec = make_sym_vector("d", dim) + sympy_conv = SympyToPymbolicMapper() + + expr = kernel.get_expression(sym_vec) + vec = [] + for i in range(len(mis)): + vec.append(evalf(expr.xreplace(dict((k, v) for + k, v in zip(sym_vec, rand[:, i]))))) + vec = sym.Matrix(vec) + result = [] + const = 0 + logger.debug("%s = ", kernel) + + sol = lu_solve_with_expand(L, U, perm, vec) + for i, coeff in enumerate(sol): + coeff = chop(coeff, tol) + if coeff == 0: + continue + if mis[i] != (-1, -1, -1): + coeff *= kernel.get_global_scaling_const() + coeff /= base_kernel.get_global_scaling_const() + result.append((mis[i], sympy_conv(coeff))) + logger.debug(" + %s.diff(%s)*%s", base_kernel, mis[i], coeff) + else: + const = sympy_conv(coeff * kernel.get_global_scaling_const()) + logger.debug(" + %s", const) + return (const, result) + + +@memoize_on_first_arg +def _get_base_kernel_matrix(base_kernel, order=None, retries=3): + dim = base_kernel.dim + + pde = base_kernel.get_pde_as_diff_op() + if order is None: + order = pde.order + + if order > pde.order: + raise NotImplementedError(f"order ({order}) cannot be greater than the order" + f"of the PDE ({pde.order}) yet.") + + mis = sorted(gnitstam(order, dim), key=sum) + # (-1, -1, -1) represent a constant + mis.append((-1, -1, -1)) + + if order == pde.order: + pde_mis = [ident.mi for eq in pde.eqs for ident in eq.keys()] + pde_mis = [mi for mi in pde_mis if sum(mi) == order] + logger.debug(f"Removing {pde_mis[-1]} to avoid linear dependent mis") + mis.remove(pde_mis[-1]) + + rand = np.random.randint(1, 10**15, (dim, len(mis))) + rand = rand.astype(object) + for i in range(rand.shape[0]): + for j in range(rand.shape[1]): + rand[i, j] = sym.sympify(rand[i, j])/10**15 + sym_vec = make_sym_vector("d", dim) + + base_expr = base_kernel.get_expression(sym_vec) + + mat = [] + for rand_vec_idx in range(rand.shape[1]): + row = [] + for mi in mis[:-1]: + expr = base_expr + for var_idx, nderivs in enumerate(mi): + if nderivs == 0: + continue + expr = expr.diff(sym_vec[var_idx], nderivs) + replace_dict = dict( + (k, v) for k, v in zip(sym_vec, rand[:, rand_vec_idx]) + ) + eval_expr = evalf(expr.xreplace(replace_dict)) + row.append(eval_expr) + row.append(1) + mat.append(row) + + mat = sym.Matrix(mat) + failed = False + try: + L, U, perm = mat.LUdecomposition() + except RuntimeError: + # symengine throws an error when rank deficient + # and sympy returns U with last row zero + failed = True + + if not sym.USE_SYMENGINE and all(expr == 0 for expr in U[-1, :]): + failed = True + + if failed: + if retries == 0: + raise RuntimeError("Failed to find a base kernel") + return _get_base_kernel_matrix( + base_kernel=base_kernel, + order=order, + retries=retries-1, + ) + + return (L, U, perm), rand, mis + + +def evalf(expr, prec=100): + """evaluate an expression numerically using ``prec`` + number of bits. + """ + from sumpy.symbolic import USE_SYMENGINE + if USE_SYMENGINE: + return expr.n(prec=prec) + else: + import sympy + dps = int(sympy.log(2**prec, 10)) + return expr.n(n=dps) + + +def filter_kernel_arguments(knls, kernel_arguments): + """From a dictionary of kernel arguments, filter out arguments + that are not needed for the kernels given as a list and return a new + dictionary. + """ + kernel_arg_names = set() + + for kernel in knls: + for karg in (kernel.get_args() + kernel.get_source_args()): + kernel_arg_names.add(karg.loopy_arg.name) + + return {k: v for (k, v) in kernel_arguments.items() if k in kernel_arg_names} + +# }}} + + +def merge_int_g_exprs(exprs, source_dependent_variables=None): """ Merge expressions involving :class:`~pytential.symbolic.primitives.IntG` objects. Several techniques are used for merging and reducing number of FMMs - * When *base_kernel* is given an *IntG* is rewritten using *base_kernel* - and its derivatives. (For example, if *base_kernel* is the biharmonic - kernel, and a Laplace kernel is encountered, this will (forcibly) - rewrite the kernel in terms of that. The routine will fail if this - process cannot be completed.) - * :class:`sumpy.kernel.AxisTargetDerivative` instances are converted to :class:`sumpy.kernel.AxisSourceDerivative` instances. (by flipping signs, assuming translation-invariance). @@ -98,11 +396,6 @@ def merge_int_g_exprs(exprs, base_kernel=None, source_dependent_variables=None): variables as dependent on source. This is important when reducing the number of FMMs needed for the output. """ - - if base_kernel is not None: - mapper = RewriteUsingBaseKernelMapper(base_kernel) - exprs = [mapper(expr) for expr in exprs] - from sumpy.assignment_collection import SymbolicAssignmentCollection seen_normal_vectors = SymbolicAssignmentCollection() @@ -246,6 +539,19 @@ def add_int_gs_in_expr(expr): return [mapper(expr) for expr in result] +class IntGCoefficientCollector(CoefficientCollector): + def __init__(self): + super().__init__({}) + + def map_int_g(self, expr): + return {expr: 1} + + def map_algebraic_leaf(self, expr, *args, **kwargs): + return {1: expr} + + handle_unsupported_expression = map_algebraic_leaf + + def is_expr_target_dependent(expr): mapper = IsExprTargetDependent() return mapper(expr) @@ -277,30 +583,6 @@ def map_q_weight(self, expr): return True -class RewriteUsingBaseKernelMapper(IdentityMapper): - """Rewrites IntGs using the base kernel. First this method replaces - IntGs with :class:`sumpy.kernel.AxisTargetDerivative` to IntGs - :class:`sumpy.kernel.AxisSourceDerivative` and then replaces - IntGs with :class:`sumpy.kernel.TargetPointMultiplier` to IntGs - without them using :class:`sumpy.kernel.ExpressionKernel` - and then finally converts them to the base kernel by finding - a relationship between the derivatives. - """ - def __init__(self, base_kernel): - self.base_kernel = base_kernel - - def map_int_g(self, expr): - # First convert IntGs with target derivatives to source derivatives - expr = convert_target_deriv_to_source(expr) - # Convert IntGs with TargetMultiplier to a sum of IntGs without - # TargetMultipliers - new_int_gs = convert_target_multiplier_to_source(expr) - # Convert IntGs with different kernels to expressions containing - # IntGs with base_kernel or its derivatives - return sum(convert_int_g_to_base(new_int_g, - self.base_kernel) for new_int_g in new_int_gs) - - def make_normal_vector_names_unique(expr, sac): mapper = MakeNormalVectorNamesUniqueMapper(sac) return mapper(expr) @@ -398,206 +680,68 @@ def get_int_g_source_group_identifier(int_g, normal_vectors): def get_int_g_target_group_identifier(int_g): - """Return a group for the *int_g* with so that all elements in that - group have the same source attributes. - """ - return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel) - - -class IntGSubstitutor(IdentityMapper): - """Replaces IntGs with pymbolic expression given by the - replacements dictionary - """ - def __init__(self, replacements): - self.replacements = replacements - - def map_int_g(self, expr): - if expr in self.replacements: - new_expr = self.replacements[expr] - if new_expr != expr: - return self.rec(new_expr) - else: - expr = new_expr - - densities = [self.rec(density) for density in expr.densities] - return expr.copy(densities=tuple(densities)) - - -def remove_target_attributes(int_g): - """Remove target attributes from *int_g* and return an expression - that is common to all expression in the same source group. - """ - return int_g.copy(target=None, qbx_forced_limit=None, - target_kernel=int_g.target_kernel.get_base_kernel()) - - -def restore_target_attributes(expr, orig_int_g): - """Restore target attributes from *orig_int_g* to all the - :class:`~pytential.symbolic.primitives.IntG` objects in the - input *expr*. - """ - int_gs = get_int_g_s([expr]) - - replacements = { - int_g: int_g.copy(target=orig_int_g.target, - qbx_forced_limit=orig_int_g.qbx_forced_limit, - target_kernel=orig_int_g.target_kernel.replace_base_kernel( - int_g.target_kernel)) - for int_g in int_gs} - - substitutor = IntGSubstitutor(replacements) - return substitutor(expr) - - -def merge_two_int_gs(int_g_1, int_g_2): - kernel_arguments = merge_kernel_arguments(int_g_1.kernel_arguments, - int_g_2.kernel_arguments) - source_kernels = int_g_1.source_kernels + int_g_2.source_kernels - densities = int_g_1.densities + int_g_2.densities - - return int_g_1.copy( - source_kernels=tuple(source_kernels), - densities=tuple(densities), - kernel_arguments=kernel_arguments, - ) - - -class IntGCoefficientCollector(CoefficientCollector): - def __init__(self): - super().__init__({}) - - def map_int_g(self, expr): - return {expr: 1} - - def map_algebraic_leaf(self, expr, *args, **kwargs): - return {1: expr} - - handle_unsupported_expression = map_algebraic_leaf - - -def evalf(expr, prec=100): - """evaluate an expression numerically using ``prec`` - number of bits. - """ - from sumpy.symbolic import USE_SYMENGINE - if USE_SYMENGINE: - return expr.n(prec=prec) - else: - import sympy - dps = int(sympy.log(2**prec, 10)) - return expr.n(n=dps) - - -@memoize_on_first_arg -def _get_base_kernel_matrix(base_kernel, order=None, retries=3): - dim = base_kernel.dim - - pde = base_kernel.get_pde_as_diff_op() - if order is None: - order = pde.order - - if order > pde.order: - raise NotImplementedError(f"order ({order}) cannot be greater than the order" - f"of the PDE ({pde.order}) yet.") - - mis = sorted(gnitstam(order, dim), key=sum) - # (-1, -1, -1) represent a constant - mis.append((-1, -1, -1)) - - if order == pde.order: - pde_mis = [ident.mi for eq in pde.eqs for ident in eq.keys()] - pde_mis = [mi for mi in pde_mis if sum(mi) == order] - logger.debug(f"Removing {pde_mis[-1]} to avoid linear dependent mis") - mis.remove(pde_mis[-1]) - - rand = np.random.randint(1, 10**15, (dim, len(mis))) - rand = rand.astype(object) - for i in range(rand.shape[0]): - for j in range(rand.shape[1]): - rand[i, j] = sym.sympify(rand[i, j])/10**15 - sym_vec = make_sym_vector("d", dim) + """Return a group for the *int_g* with so that all elements in that + group have the same source attributes. + """ + return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel) - base_expr = base_kernel.get_expression(sym_vec) - mat = [] - for rand_vec_idx in range(rand.shape[1]): - row = [] - for mi in mis[:-1]: - expr = base_expr - for var_idx, nderivs in enumerate(mi): - if nderivs == 0: - continue - expr = expr.diff(sym_vec[var_idx], nderivs) - replace_dict = dict( - (k, v) for k, v in zip(sym_vec, rand[:, rand_vec_idx]) - ) - eval_expr = evalf(expr.xreplace(replace_dict)) - row.append(eval_expr) - row.append(1) - mat.append(row) +class IntGSubstitutor(IdentityMapper): + """Replaces IntGs with pymbolic expression given by the + replacements dictionary + """ + def __init__(self, replacements): + self.replacements = replacements - mat = sym.Matrix(mat) - failed = False - try: - L, U, perm = mat.LUdecomposition() - except RuntimeError: - # symengine throws an error when rank deficient - # and sympy returns U with last row zero - failed = True + def map_int_g(self, expr): + if expr in self.replacements: + new_expr = self.replacements[expr] + if new_expr != expr: + return self.rec(new_expr) + else: + expr = new_expr - if not sym.USE_SYMENGINE and all(expr == 0 for expr in U[-1, :]): - failed = True + densities = [self.rec(density) for density in expr.densities] + return expr.copy(densities=tuple(densities)) - if failed: - if retries == 0: - raise RuntimeError("Failed to find a base kernel") - return _get_base_kernel_matrix( - base_kernel=base_kernel, - order=order, - retries=retries-1, - ) - return (L, U, perm), rand, mis +def remove_target_attributes(int_g): + """Remove target attributes from *int_g* and return an expression + that is common to all expression in the same source group. + """ + return int_g.copy(target=None, qbx_forced_limit=None, + target_kernel=int_g.target_kernel.get_base_kernel()) -@memoize_on_first_arg -def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None): - (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order) - dim = base_kernel.dim - sym_vec = make_sym_vector("d", dim) - sympy_conv = SympyToPymbolicMapper() +def restore_target_attributes(expr, orig_int_g): + """Restore target attributes from *orig_int_g* to all the + :class:`~pytential.symbolic.primitives.IntG` objects in the + input *expr*. + """ + int_gs = get_int_g_s([expr]) - expr = kernel.get_expression(sym_vec) - vec = [] - for i in range(len(mis)): - vec.append(evalf(expr.xreplace(dict((k, v) for - k, v in zip(sym_vec, rand[:, i]))))) - vec = sym.Matrix(vec) - result = [] - const = 0 - logger.debug("%s = ", kernel) + replacements = { + int_g: int_g.copy(target=orig_int_g.target, + qbx_forced_limit=orig_int_g.qbx_forced_limit, + target_kernel=orig_int_g.target_kernel.replace_base_kernel( + int_g.target_kernel)) + for int_g in int_gs} - sol = lu_solve_with_expand(L, U, perm, vec) - for i, coeff in enumerate(sol): - coeff = chop(coeff, tol) - if coeff == 0: - continue - if mis[i] != (-1, -1, -1): - coeff *= kernel.get_global_scaling_const() - coeff /= base_kernel.get_global_scaling_const() - result.append((mis[i], sympy_conv(coeff))) - logger.debug(" + %s.diff(%s)*%s", base_kernel, mis[i], coeff) - else: - const = sympy_conv(coeff * kernel.get_global_scaling_const()) - logger.debug(" + %s", const) - return (const, result) + substitutor = IntGSubstitutor(replacements) + return substitutor(expr) -def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None): - res = [] - for knl in kernels: - res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order)) - return res +def merge_two_int_gs(int_g_1, int_g_2): + kernel_arguments = merge_kernel_arguments(int_g_1.kernel_arguments, + int_g_2.kernel_arguments) + source_kernels = int_g_1.source_kernels + int_g_2.source_kernels + densities = int_g_1.densities + int_g_2.densities + + return int_g_1.copy( + source_kernels=tuple(source_kernels), + densities=tuple(densities), + kernel_arguments=kernel_arguments, + ) class GetIntGs(WalkMapper): @@ -635,142 +779,6 @@ def get_int_g_s(exprs): return get_int_g_mapper.int_g_s -def filter_kernel_arguments(knls, kernel_arguments): - """From a dictionary of kernel arguments, filter out arguments - that are not needed for the kernels given as a list and return a new - dictionary. - """ - kernel_arg_names = set() - - for kernel in knls: - for karg in (kernel.get_args() + kernel.get_source_args()): - kernel_arg_names.add(karg.loopy_arg.name) - - return {k: v for (k, v) in kernel_arguments.items() if k in kernel_arg_names} - - -def convert_int_g_to_base(int_g, base_kernel): - result = 0 - for knl, density in zip(int_g.source_kernels, int_g.densities): - result += _convert_int_g_to_base( - int_g.copy(source_kernels=(knl,), densities=(density,)), - base_kernel) - return result - - -def _convert_int_g_to_base(int_g, base_kernel): - target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) - dim = target_kernel.dim - - result = 0 - for density, source_kernel in zip(int_g.densities, int_g.source_kernels): - deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), - base_kernel) - - const = deriv_relation[0] - # NOTE: we set a dofdesc here to force the evaluation of this integral - # on the source instead of the target when using automatic tagging - # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` - dd = pytential.sym.DOFDescriptor(None, - discr_stage=pytential.sym.QBX_SOURCE_STAGE1) - const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) - - if const != 0 and target_kernel != target_kernel.get_base_kernel(): - # There might be some TargetPointMultipliers hanging around. - # FIXME: handle them instead of bailing out - return [int_g] - - if source_kernel != source_kernel.get_base_kernel(): - # We assume that any source transformation is a derivative - # and the constant when applied becomes zero. - const = 0 - - result += const - - new_kernel_args = filter_kernel_arguments([base_kernel], - int_g.kernel_arguments) - - for mi, c in deriv_relation[1]: - knl = source_kernel.replace_base_kernel(base_kernel) - for d, val in enumerate(mi): - for _ in range(val): - knl = AxisSourceDerivative(d, knl) - c *= -1 - result += int_g.copy(source_kernels=(knl,), target_kernel=target_kernel, - densities=(density,), kernel_arguments=new_kernel_args) * c - return result - - -def convert_target_multiplier_to_source(int_g): - """Convert an IntG with TargetMultiplier to an sum of IntGs without - TargetMultiplier and only source dependent transformations - """ - from sumpy.symbolic import SympyToPymbolicMapper - tgt_knl = int_g.target_kernel - if not isinstance(tgt_knl, TargetPointMultiplier): - return [int_g] - if isinstance(tgt_knl.inner_kernel, KernelWrapper): - return [int_g] - - new_kernel_args = filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) - result = [] - # If the kernel is G, source is y and target is x, - # x G = y*G + (x - y)*G - # For y*G, absorb y into a density - new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) - for density in int_g.densities] - result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, - densities=tuple(new_densities), kernel_arguments=new_kernel_args)) - - # create a new expression kernel for (x - y)*G - sym_d = make_sym_vector("d", tgt_knl.dim) - conv = SympyToPymbolicMapper() - - for knl, density in zip(int_g.source_kernels, int_g.densities): - new_expr = conv(knl.postprocess_at_source(knl.get_expression(sym_d), sym_d) - * sym_d[tgt_knl.axis]) - new_knl = ExpressionKernel(knl.dim, new_expr, - knl.get_base_kernel().global_scaling_const, - knl.is_complex_valued) - result.append(int_g.copy(target_kernel=new_knl, - densities=(density,), - source_kernels=(new_knl,), kernel_arguments=new_kernel_args)) - return result - - -def convert_target_deriv_to_source(int_g): - """Converts AxisTargetDerivatives to AxisSourceDerivative instances - from an IntG. If there are outer TargetPointMultiplier transformations - they are preserved. - """ - knl = int_g.target_kernel - source_kernels = list(int_g.source_kernels) - coeff = 1 - multipliers = [] - while isinstance(knl, TargetPointMultiplier): - multipliers.append(knl.axis) - knl = knl.inner_kernel - - while isinstance(knl, AxisTargetDerivative): - coeff *= -1 - source_kernels = [AxisSourceDerivative(knl.axis, source_knl) for - source_knl in source_kernels] - knl = knl.inner_kernel - - # TargetPointMultiplier has to be the outermost kernel - # If it is the inner kernel, return early - if isinstance(knl, TargetPointMultiplier): - return 1, int_g - - for axis in reversed(multipliers): - knl = TargetPointMultiplier(axis, knl) - - new_densities = tuple(density*coeff for density in int_g.densities) - return int_g.copy(target_kernel=knl, - densities=new_densities, - source_kernels=tuple(source_kernels)) - - def convert_directional_source_to_axis_source(int_g, source_dependent_variables): """Convert an IntG with a DirectionalSourceDerivative instance to an IntG with d AxisSourceDerivative instances. diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index ec7ee3a35..4d3614c48 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -23,7 +23,7 @@ import numpy as np from pytential import sym -from pytential.symbolic.pde.system_utils import merge_int_g_exprs +from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel from sumpy.kernel import (StressletKernel, LaplaceKernel, ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) @@ -106,7 +106,7 @@ def apply_pressure(self, density_vec_sym, qbx_forced_limit): sym.S(kernel, density_vec_sym[i], qbx_forced_limit=qbx_forced_limit))) - return merge_int_g_exprs(sym_expr) + return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): """Symbolic derivative of velocity from Stokeslet. @@ -217,7 +217,7 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): density_vec_sym[i] * dir_vec_sym[j], qbx_forced_limit=qbx_forced_limit))) - return merge_int_g_exprs(sym_expr) + return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -327,7 +327,10 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): density_vec_sym[i], [1]*self.dim, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - return merge_int_g_exprs(sym_expr, base_kernel=self.base_kernel) + if self.base_kernel is None: + return sym_expr + else: + return rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel) def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -444,7 +447,10 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, density_vec_sym[i], dir_vec_sym, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - return merge_int_g_exprs(sym_expr, base_kernel=self.base_kernel) + if self.base_kernel is None: + return sym_expr + else: + return rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, @@ -462,7 +468,7 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, sym_expr += stokeslet_obj.apply(stokeslet_density_vec_sym, qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight - return merge_int_g_exprs(sym_expr) + return sym_expr def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -822,14 +828,13 @@ def prepare_rhs(self, b): def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. K. 1985 Equation 2.18 - return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator( - sigma, normal, qbx_forced_limit)) + return -0.5 * self.side * sigma - self._operator( + sigma, normal, qbx_forced_limit) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. K. 1985 Equation 2.16 - return merge_int_g_exprs( - -self._farfield(qbx_forced_limit) - - self._operator(sigma, normal, qbx_forced_limit)) + return -self._farfield(qbx_forced_limit) \ + - self._operator(sigma, normal, qbx_forced_limit) def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: H. K. 1985 Equation 2.17 @@ -875,13 +880,12 @@ def _operator(self, sigma, normal, qbx_forced_limit): def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 - return merge_int_g_exprs(-0.5 * self.side * sigma - self._operator(sigma, - normal, qbx_forced_limit)) + return -0.5 * self.side * sigma - self._operator(sigma, + normal, qbx_forced_limit) def velocity(self, sigma, *, normal, qbx_forced_limit=2): # NOTE: H. 1986 Equation 16 - return merge_int_g_exprs(-self._operator(sigma, normal, - qbx_forced_limit)) + return -self._operator(sigma, normal, qbx_forced_limit) def pressure(self, sigma, *, normal, qbx_forced_limit=2): # FIXME: not given in H. 1986, but should be easy to derive using the diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index a54b1ef78..da519d1ff 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -24,7 +24,8 @@ AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel, HelmholtzKernel) from pytential.symbolic.primitives import (int_g_vec, D, IntG, NodeCoordinateComponent) -from pytential.symbolic.pde.system_utils import merge_int_g_exprs +from pytential.symbolic.pde.system_utils import (merge_int_g_exprs, + rewrite_using_base_kernel) from pymbolic.primitives import make_sym_vector, Variable import pytest @@ -127,9 +128,9 @@ def test_base_kernel_merge(): int_g_vec(TargetPointMultiplier(1, knl), density, qbx_forced_limit=1) - result = merge_int_g_exprs([int_g1, int_g2], - source_dependent_variables=[], + exprs_rewritten = rewrite_using_base_kernel([int_g1, int_g2], base_kernel=biharm_knl) + result = merge_int_g_exprs(exprs_rewritten, source_dependent_variables=[]) sources = [NodeCoordinateComponent(i) for i in range(dim)] @@ -261,6 +262,8 @@ def test_restoring_target_attributes(): # $ python test_pde_system_tools.py 'test_reduce_number_of_fmms()' if __name__ == "__main__": + import logging + logging.basicConfig(level=logging.DEBUG) import sys if len(sys.argv) > 1: exec(sys.argv[1]) From 00b044197ebc7a68c5488ba373ff273f7ef767ae Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 9 Oct 2021 16:55:17 -0500 Subject: [PATCH 185/209] Fix IntGs within IntGs and add a test --- pytential/symbolic/pde/reduce_fmms.py | 9 +++++++ test/test_pde_system_utils.py | 36 +++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 4cabab23f..c680dddc9 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -83,6 +83,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): # transformations. assert _check_int_gs_common(int_gs) + from pytential.symbolic.pde.system_utils import get_int_g_s + source_dependent_variables = list(source_dependent_variables) + for int_g in int_gs: + for density in int_g.densities: + for inner_int_g in get_int_g_s([density]): + if inner_int_g not in source_dependent_variables: + source_dependent_variables.append(inner_int_g) + try: mat, source_exprs = _create_matrix(int_gs, source_dependent_variables, axis_vars) @@ -91,6 +99,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): return int_gs mat = sympy.Matrix(mat) + # Factor the matrix into two try: left_factor = _factor_left(mat, axis_vars) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index da519d1ff..35bde4361 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -258,6 +258,42 @@ def test_restoring_target_attributes(): assert result[1] == int_g2 +def test_int_gs_in_densities(): + from pymbolic.primitives import Variable, Quotient + dim = 3 + laplace_knl = LaplaceKernel(dim) + density = Variable("density") + + int_g1 = \ + int_g_vec(laplace_knl, + int_g_vec(AxisSourceDerivative(2, laplace_knl), density, + qbx_forced_limit=1), qbx_forced_limit=1) + \ + int_g_vec(AxisTargetDerivative(0, laplace_knl), + int_g_vec(AxisSourceDerivative(1, laplace_knl), 2*density, + qbx_forced_limit=1), qbx_forced_limit=1) + + # In the above example the two inner source derivatives should + # be converted to target derivatives and the two outermost + # IntGs should be merged into one by converting the target + # derivative in the last term to a source derivative + result = merge_int_g_exprs([int_g1], + source_dependent_variables=[]) + + source_kernels = [AxisSourceDerivative(0, laplace_knl), laplace_knl] + densities = [ + (-1)*int_g_vec(AxisTargetDerivative(1, laplace_knl), + (-2)*density, qbx_forced_limit=1), + int_g_vec(AxisTargetDerivative(2, laplace_knl), + (-2)*density, qbx_forced_limit=1) * Quotient(1, 2) + ] + int_g3 = IntG(target_kernel=laplace_knl, + source_kernels=tuple(source_kernels), + densities=tuple(densities), + qbx_forced_limit=1) + + assert result[0] == int_g3 + + # You can test individual routines by typing # $ python test_pde_system_tools.py 'test_reduce_number_of_fmms()' From 78bbd1abdde51134e964699eba6b350adbf962c0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 10:11:11 -0500 Subject: [PATCH 186/209] Mark all variables as source dependent if not given --- pytential/symbolic/pde/reduce_fmms.py | 39 +++++++++++++++++++++----- pytential/symbolic/pde/system_utils.py | 24 ++++------------ test/test_pde_system_utils.py | 17 +++-------- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index c680dddc9..d6152920e 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -25,6 +25,7 @@ from pymbolic.interop.sympy import PymbolicToSympyMapper, SympyToPymbolicMapper from pymbolic.mapper import Mapper +from pymbolic.geometric_algebra.mapper import WalkMapper from pymbolic.primitives import Product import sympy import functools @@ -83,13 +84,8 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): # transformations. assert _check_int_gs_common(int_gs) - from pytential.symbolic.pde.system_utils import get_int_g_s - source_dependent_variables = list(source_dependent_variables) - for int_g in int_gs: - for density in int_g.densities: - for inner_int_g in get_int_g_s([density]): - if inner_int_g not in source_dependent_variables: - source_dependent_variables.append(inner_int_g) + if source_dependent_variables is None: + source_dependent_variables = get_all_source_dependent_variables(int_gs) try: mat, source_exprs = _create_matrix(int_gs, source_dependent_variables, @@ -158,6 +154,35 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): return res +class GatherAllSourceDependentVariables(WalkMapper): + def __init__(self): + self.vars = {} + + def map_variable(self, expr): + from sumpy.symbolic import SpatialConstant + if not isinstance(expr, SpatialConstant): + self.vars[expr] = True + + def map_node_coordinate_component(self, expr): + self.vars[expr] = True + + def map_list(self, exprs): + for expr in exprs: + self.rec(expr) + + def map_int_g(self, expr): + self.vars[expr] = True + for density in expr.densities: + self.rec(density) + + +def get_all_source_dependent_variables(int_gs): + mapper = GatherAllSourceDependentVariables() + for int_g in int_gs: + for density in int_g.densities: + mapper(density) + return list(mapper.vars.keys()) + # }}} # {{{ convert IntG expressions to a matrix diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 99dd74987..0c8f7918a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -437,9 +437,7 @@ def add_int_gs_in_expr(expr): int_g = make_normal_vector_names_unique(int_g, seen_normal_vectors) # convert DirectionalSourceDerivative to AxisSourceDerivative # as kernel arguments need to be the same for merging - if source_dependent_variables is not None: - int_g = convert_directional_source_to_axis_source(int_g, - source_dependent_variables) + int_g = convert_directional_source_to_axis_source(int_g) # convert TargetDerivative to source before checking the group # as the target kernel has to be the same for merging int_g = convert_target_deriv_to_source(int_g) @@ -481,12 +479,6 @@ def add_int_gs_in_expr(expr): if all(not int_gs for int_gs in int_gs_by_source_group): return exprs - # If source_dependent_variables (sdv) is not given return early. - # Check for (sdv is None) instead of just (sdv) because - # it can be an empty list. - if source_dependent_variables is None: - return result - # Do the calculation for each source_group_identifier separately # and assemble them replacements = {} @@ -779,15 +771,14 @@ def get_int_g_s(exprs): return get_int_g_mapper.int_g_s -def convert_directional_source_to_axis_source(int_g, source_dependent_variables): +def convert_directional_source_to_axis_source(int_g): """Convert an IntG with a DirectionalSourceDerivative instance to an IntG with d AxisSourceDerivative instances. """ source_kernels = [] densities = [] for source_kernel, density in zip(int_g.source_kernels, int_g.densities): - knl_result = _convert_directional_source_knl_to_axis_source(source_kernel, - source_dependent_variables) + knl_result = _convert_directional_source_knl_to_axis_source(source_kernel) for knl, coeff in knl_result: source_kernels.append(knl) densities.append(coeff * density) @@ -795,25 +786,22 @@ def convert_directional_source_to_axis_source(int_g, source_dependent_variables) densities=tuple(densities)) -def _convert_directional_source_knl_to_axis_source(knl, source_dependent_variables): +def _convert_directional_source_knl_to_axis_source(knl): if isinstance(knl, DirectionalSourceDerivative): dim = knl.dim from pymbolic import make_sym_vector dir_vec = make_sym_vector(knl.dir_vec_name, dim) - if Variable(knl.dir_vec_name) not in source_dependent_variables: - raise ValueError(f"{knl.dir_vec_name} not given in " - "source_dependent_variables, but is dependent on source") res = [] inner_result = _convert_directional_source_knl_to_axis_source( - knl.inner_kernel, source_dependent_variables) + knl.inner_kernel) for inner_knl, coeff in inner_result: for d in range(dim): res.append((AxisSourceDerivative(d, inner_knl), coeff*dir_vec[d])) return res elif isinstance(knl, KernelWrapper): inner_result = _convert_directional_source_knl_to_axis_source( - knl.inner_kernel, source_dependent_variables) + knl.inner_kernel) return [(knl.replace_inner_kernel(inner_knl), coeff) for inner_knl, coeff in inner_result] else: diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 35bde4361..e30d7f518 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -219,25 +219,17 @@ def test_merge_directional_source(): int_g1 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) int_g2 = D(laplace_knl, density, qbx_forced_limit=1) - with pytest.raises(ValueError): - result = merge_int_g_exprs([int_g1 + int_g2], - source_dependent_variables=[]) - - result = merge_int_g_exprs([int_g1 + int_g2], - source_dependent_variables=[dsource]) - source_kernels = [AxisSourceDerivative(d, laplace_knl) for d in range(dim)] + [laplace_knl] densities = [density*dsource[d] for d in range(dim)] + [density] int_g3 = int_g2.copy(source_kernels=source_kernels, densities=densities) + result = merge_int_g_exprs([int_g1 + int_g2], + source_dependent_variables=[dsource, density]) assert result[0] == int_g3 result = merge_int_g_exprs([int_g1 + int_g2]) - int_g4 = int_g2.copy( - source_kernels=int_g2.source_kernels + int_g1.source_kernels, - densities=(density, density)) - assert result[0] == int_g4 + assert result[0] == int_g3 def test_restoring_target_attributes(): @@ -276,8 +268,7 @@ def test_int_gs_in_densities(): # be converted to target derivatives and the two outermost # IntGs should be merged into one by converting the target # derivative in the last term to a source derivative - result = merge_int_g_exprs([int_g1], - source_dependent_variables=[]) + result = merge_int_g_exprs([int_g1]) source_kernels = [AxisSourceDerivative(0, laplace_knl), laplace_knl] densities = [ From 85df49de90e6b32520d82338412fbfe2b1610b76 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 14:06:35 -0500 Subject: [PATCH 187/209] Fix handling DirectionalSourceDerivative --- pytential/symbolic/pde/reduce_fmms.py | 31 ++++++++++++++++++++---- pytential/symbolic/pde/system_utils.py | 33 ++++++++++++++++++++------ test/test_pde_system_utils.py | 11 +++++---- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index d6152920e..44150faf3 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -163,9 +163,6 @@ def map_variable(self, expr): if not isinstance(expr, SpatialConstant): self.vars[expr] = True - def map_node_coordinate_component(self, expr): - self.vars[expr] = True - def map_list(self, exprs): for expr in exprs: self.rec(expr) @@ -175,6 +172,12 @@ def map_int_g(self, expr): for density in expr.densities: self.rec(density) + def map_node_coordinate_component(self, expr): + self.vars[expr] = True + + map_num_reference_derivative = map_node_coordinate_component + map_interpolation = map_node_coordinate_component + def get_all_source_dependent_variables(int_gs): mapper = GatherAllSourceDependentVariables() @@ -192,15 +195,28 @@ def _check_int_gs_common(int_gs): have the same base kernel and other properties that would allow merging them. """ + from pytential.symbolic.pde.system_utils import merge_kernel_arguments + + kernel_arguments = {} base_kernel = int_gs[0].source_kernels[0].get_base_kernel() common_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) + for int_g in int_gs: for source_kernel in int_g.source_kernels: if source_kernel.get_base_kernel() != base_kernel: return False - if common_int_g != int_g.copy(target_kernel=base_kernel, - source_kernels=(base_kernel,), densities=(1,)): + + if common_int_g.qbx_forced_limit != int_g.qbx_forced_limit: + return False + + if common_int_g.source != int_g.source: + return False + + try: + kernel_arguments = merge_kernel_arguments(kernel_arguments, + int_g.kernel_arguments) + except ValueError: return False return True @@ -281,6 +297,11 @@ def map_algebraic_leaf(self, expr): def map_node_coordinate_component(self, expr): return {expr: 1} + map_num_reference_derivative = map_node_coordinate_component + + def map_common_subexpression(self, expr): + return {expr: 1} + def map_subscript(self, expr): if expr in self.source_dependent_variables or \ expr.aggregate in self.source_dependent_variables: diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 0c8f7918a..1f3881e5e 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -528,7 +528,22 @@ def add_int_gs_in_expr(expr): replacements[int_g] = restore_target_attributes(reduced_insn, int_g) mapper = IntGSubstitutor(replacements) - return [mapper(expr) for expr in result] + result = [mapper(expr) for expr in result] + + orig_count = get_number_of_fmms(exprs) + new_count = get_number_of_fmms(result) + if orig_count < new_count: + raise RuntimeError("merge_int_g_exprs failed. " + "Please open an issue in pytential bug tracker.") + + return result + + +def get_number_of_fmms(exprs): + fmms = set() + for int_g in get_int_g_s(exprs): + fmms.add(remove_target_attributes(int_g)) + return len(fmms) class IntGCoefficientCollector(CoefficientCollector): @@ -778,30 +793,34 @@ def convert_directional_source_to_axis_source(int_g): source_kernels = [] densities = [] for source_kernel, density in zip(int_g.source_kernels, int_g.densities): - knl_result = _convert_directional_source_knl_to_axis_source(source_kernel) + knl_result = _convert_directional_source_knl_to_axis_source(source_kernel, + int_g.kernel_arguments) for knl, coeff in knl_result: source_kernels.append(knl) densities.append(coeff * density) + + kernel_arguments = filter_kernel_arguments( + list(source_kernels) + [int_g.target_kernel], int_g.kernel_arguments) return int_g.copy(source_kernels=tuple(source_kernels), - densities=tuple(densities)) + densities=tuple(densities), kernel_arguments=kernel_arguments) -def _convert_directional_source_knl_to_axis_source(knl): +def _convert_directional_source_knl_to_axis_source(knl, knl_arguments): if isinstance(knl, DirectionalSourceDerivative): dim = knl.dim from pymbolic import make_sym_vector - dir_vec = make_sym_vector(knl.dir_vec_name, dim) + dir_vec = knl_arguments[knl.dir_vec_name] res = [] inner_result = _convert_directional_source_knl_to_axis_source( - knl.inner_kernel) + knl.inner_kernel, knl_arguments) for inner_knl, coeff in inner_result: for d in range(dim): res.append((AxisSourceDerivative(d, inner_knl), coeff*dir_vec[d])) return res elif isinstance(knl, KernelWrapper): inner_result = _convert_directional_source_knl_to_axis_source( - knl.inner_kernel) + knl.inner_kernel, knl_arguments) return [(knl.replace_inner_kernel(inner_knl), coeff) for inner_knl, coeff in inner_result] else: diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index e30d7f518..97d22e4b9 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -211,21 +211,24 @@ def test_merge_different_qbx_forced_limit(): def test_merge_directional_source(): from pymbolic.primitives import Variable + from pytential.symbolic.primitives import cse + dim = 3 laplace_knl = LaplaceKernel(dim) density = Variable("density") - dsource = Variable("dsource_vec") int_g1 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) int_g2 = D(laplace_knl, density, qbx_forced_limit=1) source_kernels = [AxisSourceDerivative(d, laplace_knl) for d in range(dim)] + [laplace_knl] - densities = [density*dsource[d] for d in range(dim)] + [density] - int_g3 = int_g2.copy(source_kernels=source_kernels, densities=densities) + dsource = int_g2.kernel_arguments["dsource_vec"] + densities = [dsource[d]*cse(density) for d in range(dim)] + [density] + int_g3 = int_g2.copy(source_kernels=source_kernels, densities=densities, + kernel_arguments={}) result = merge_int_g_exprs([int_g1 + int_g2], - source_dependent_variables=[dsource, density]) + source_dependent_variables=[density]) assert result[0] == int_g3 result = merge_int_g_exprs([int_g1 + int_g2]) From 3be0da2940e750419c632c211f81d0b2b8787447 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 14:10:04 -0500 Subject: [PATCH 188/209] Run merge_int_g_exprs by default --- pytential/symbolic/execution.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/execution.py b/pytential/symbolic/execution.py index 9b3cece06..6b671d7d1 100644 --- a/pytential/symbolic/execution.py +++ b/pytential/symbolic/execution.py @@ -1156,7 +1156,7 @@ def __call__(self, *args, **kwargs): timing_data=timing_data) -def bind(places, expr, auto_where=None): +def bind(places, expr, auto_where=None, _merge_exprs=True): """ :arg places: a :class:`pytential.GeometryCollection`. Alternatively, any list or mapping that is a valid argument for its @@ -1173,6 +1173,17 @@ def bind(places, expr, auto_where=None): places = GeometryCollection(places, auto_where=auto_where) auto_where = places.auto_where expr = _prepare_expr(places, expr, auto_where=auto_where) + if _merge_exprs: + from pytential.symbolic.pde.system_utils import merge_int_g_exprs + from pymbolic.primitives import Expression + from pytential.qbx import QBXLayerPotentialSource + fmmlib = any(value.fmm_backend == "fmmlib" for value + in places.places.values() if isinstance(value, QBXLayerPotentialSource)) + if not fmmlib: + if isinstance(expr, (np.ndarray, list, tuple)): + expr = np.array(merge_int_g_exprs(list(expr)), dtype=object) + elif isinstance(expr, Expression): + expr = merge_int_g_exprs([expr])[0] return BoundExpression(places, expr) From e4f9fb011b11370f8850e4f0da692c57fe9f00a1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 14:10:28 -0500 Subject: [PATCH 189/209] Remove unused imports --- pytential/symbolic/pde/reduce_fmms.py | 1 + pytential/symbolic/pde/system_utils.py | 2 -- test/test_pde_system_utils.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 44150faf3..a232fed04 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -188,6 +188,7 @@ def get_all_source_dependent_variables(int_gs): # }}} + # {{{ convert IntG expressions to a matrix def _check_int_gs_common(int_gs): diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 1f3881e5e..1789ae2c0 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -35,7 +35,6 @@ from pymbolic.geometric_algebra.mapper import WalkMapper from pymbolic.mapper import CombineMapper from pymbolic.mapper.coefficient import CoefficientCollector -from pymbolic.primitives import Variable from pytential.symbolic.primitives import IntG, NodeCoordinateComponent from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand @@ -808,7 +807,6 @@ def convert_directional_source_to_axis_source(int_g): def _convert_directional_source_knl_to_axis_source(knl, knl_arguments): if isinstance(knl, DirectionalSourceDerivative): dim = knl.dim - from pymbolic import make_sym_vector dir_vec = knl_arguments[knl.dir_vec_name] res = [] diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 97d22e4b9..309e27377 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -27,7 +27,6 @@ from pytential.symbolic.pde.system_utils import (merge_int_g_exprs, rewrite_using_base_kernel) from pymbolic.primitives import make_sym_vector, Variable -import pytest def test_reduce_number_of_fmms(): From 4d079c90a70ca4d7247d083ee80fadd5000454ab Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 14:15:16 -0500 Subject: [PATCH 190/209] remove unused variables --- pytential/symbolic/pde/system_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 1789ae2c0..d39a715b9 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -462,7 +462,7 @@ def add_int_gs_in_expr(expr): int_gs_by_group[group] = new_int_g # Do some simplifications after merging. Not stricty necessary - for (source_group, _, coeff), int_g in int_gs_by_group.items(): + for (_, _, coeff), int_g in int_gs_by_group.items(): # replace an IntG with d axis source derivatives to an IntG # with one directional source derivative # TODO: reenable this later @@ -481,7 +481,7 @@ def add_int_gs_in_expr(expr): # Do the calculation for each source_group_identifier separately # and assemble them replacements = {} - for source_group, int_gs in int_gs_by_source_group.items(): + for int_gs in int_gs_by_source_group.values(): # For each output, we now have a sum of int_gs with # different target attributes. # for eg: {+}S + {-}D (where {x} is the QBX limit). From 8a8a6010414e7ca3f185317a9d8bdba7a580d845 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 15:53:27 -0500 Subject: [PATCH 191/209] use get_args and get_source_args for target vs source --- pytential/symbolic/pde/system_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index d39a715b9..800cf21b8 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -681,7 +681,7 @@ def get_int_g_source_group_identifier(int_g, normal_vectors): group have the same source attributes. """ args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( - int_g.kernel_arguments.items()) if k not in normal_vectors) + int_g.kernel_arguments.items()) if k in int_g.get_source_args()) return (int_g.source, args, int_g.target_kernel.get_base_kernel()) @@ -689,7 +689,9 @@ def get_int_g_target_group_identifier(int_g): """Return a group for the *int_g* with so that all elements in that group have the same source attributes. """ - return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel) + args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( + int_g.kernel_arguments.items()) if k in int_g.get_args()) + return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel, args) class IntGSubstitutor(IdentityMapper): From 62e4dd2a9fe16bb93e9e06e47a0eaff876e3e3de Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 16:23:15 -0500 Subject: [PATCH 192/209] remove making normal vector names unique --- pytential/symbolic/pde/system_utils.py | 112 ++++--------------------- 1 file changed, 18 insertions(+), 94 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 800cf21b8..3cb4a53a9 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -395,9 +395,6 @@ def merge_int_g_exprs(exprs, source_dependent_variables=None): variables as dependent on source. This is important when reducing the number of FMMs needed for the output. """ - from sumpy.assignment_collection import SymbolicAssignmentCollection - seen_normal_vectors = SymbolicAssignmentCollection() - # Using a dictionary instead of a set because sets are unordered all_source_group_identifiers = {} @@ -408,8 +405,7 @@ def merge_int_g_exprs(exprs, source_dependent_variables=None): def add_int_gs_in_expr(expr): for int_g in get_int_g_s([expr]): - source_group_identifier = get_int_g_source_group_identifier(int_g, - seen_normal_vectors.assignments) + source_group_identifier = get_int_g_source_group_identifier(int_g) int_gs_by_source_group[source_group_identifier].append(int_g) for density in int_g.densities: add_int_gs_in_expr(density) @@ -423,7 +419,6 @@ def add_int_gs_in_expr(expr): # FIXME: if there's ever any use case, then we can extract # some IntGs from them. logger.debug("%s is not linear", expr) - expr = make_normal_vector_names_unique(expr, seen_normal_vectors) result[i] += expr add_int_gs_in_expr(expr) continue @@ -433,7 +428,6 @@ def add_int_gs_in_expr(expr): result[i] += coeff continue - int_g = make_normal_vector_names_unique(int_g, seen_normal_vectors) # convert DirectionalSourceDerivative to AxisSourceDerivative # as kernel arguments need to be the same for merging int_g = convert_directional_source_to_axis_source(int_g) @@ -446,8 +440,7 @@ def add_int_gs_in_expr(expr): int_g.densities]) coeff = 1 - source_group_identifier = get_int_g_source_group_identifier(int_g, - seen_normal_vectors.assignments) + source_group_identifier = get_int_g_source_group_identifier(int_g) target_group_identifier = get_int_g_target_group_identifier(int_g) group = (source_group_identifier, target_group_identifier, coeff) @@ -589,84 +582,6 @@ def map_q_weight(self, expr): return True -def make_normal_vector_names_unique(expr, sac): - mapper = MakeNormalVectorNamesUniqueMapper(sac) - return mapper(expr) - - -class MakeNormalVectorNamesUniqueMapper(IdentityMapper): - def __init__(self, sac): - self.sac = sac - - def map_int_g(self, int_g): - sac = self.sac - normal_vectors = get_normal_vectors(int_g) - replacements = {} - for vector_name, value in normal_vectors.items(): - if vector_name not in sac.assignments: - # There was no previous IntG with the same normal vector name - sac.assignments[vector_name] = value - sac.reversed_assignments[value] = vector_name - elif sac.assignments[vector_name] != value: - # There was a previous IntG with the same normal vector name - # and the value was different. We need to rename - new_name = sac.symbol_generator(vector_name).name - # If this name was already renamed, use that name - if value in sac.reversed_assignments: - new_name = sac.reversed_assignments[value] - else: - sac.assignments[new_name] = value - sac.reversed_assignments[value] = new_name - replacements[vector_name] = new_name - - target_kernel = rename_normal_vector_name(int_g.target_kernel, - replacements) - source_kernels = tuple([rename_normal_vector_name(source_kernel, - replacements) for source_kernel in int_g.source_kernels]) - - kernel_arguments = int_g.kernel_arguments.copy() - # first delete the old names and then add in the new names - # these have to be done in two loops to avoid issues with - # some new names conflicting with old names - for old_name in replacements.keys(): - del kernel_arguments[old_name] - for old_name, new_name in replacements.items(): - kernel_arguments[new_name] = int_g.kernel_arguments[old_name] - - densities = [self.rec(density) for density in int_g.densities] - return int_g.copy(target_kernel=target_kernel, source_kernels=source_kernels, - densities=tuple(densities)) - - -def rename_normal_vector_name(kernel, replacements): - if not isinstance(kernel, KernelWrapper): - return kernel - renamed_inner_kernel = rename_normal_vector_name(kernel.inner_kernel, - replacements) - - if not isinstance(kernel, DirectionalDerivative): - return kernel.replace_inner_kernel(renamed_inner_kernel) - - new_name = replacements.get(kernel.dir_vec_name, kernel.dir_vec_name) - return type(kernel)(renamed_inner_kernel, new_name) - - -def get_normal_vectors(int_g): - """Return the normal vector names and their values from a - *int_g*. - """ - normal_vectors = {} - kernels = [int_g.target_kernel] + list(int_g.source_kernels) - for kernel in kernels: - while isinstance(kernel, KernelWrapper): - if isinstance(kernel, DirectionalDerivative): - name = kernel.dir_vec_name - normal_vectors[name] = get_hashable_kernel_argument( - int_g.kernel_arguments[name]) - kernel = kernel.inner_kernel - return normal_vectors - - def get_hashable_kernel_argument(arg): if hasattr(arg, "__iter__"): try: @@ -676,21 +591,29 @@ def get_hashable_kernel_argument(arg): return arg -def get_int_g_source_group_identifier(int_g, normal_vectors): - """Return a group for the *int_g* with so that all elements in that +def get_int_g_source_group_identifier(int_g): + """Return a identifier for a group for the *int_g* so that all elements in that group have the same source attributes. """ + from sumpy.tools import gather_source_arguments + source_arg_names = set(arg.loopy_arg.name for arg in + gather_source_arguments(int_g.source_kernels)) args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( - int_g.kernel_arguments.items()) if k in int_g.get_source_args()) + int_g.kernel_arguments.items()) if k in source_arg_names) return (int_g.source, args, int_g.target_kernel.get_base_kernel()) def get_int_g_target_group_identifier(int_g): - """Return a group for the *int_g* with so that all elements in that - group have the same source attributes. + """Return a identifier for a group for the *int_g* so that all elements in that + group have the same target attributes. """ + from sumpy.tools import gather_arguments, gather_source_arguments + source_arg_names = set(arg.loopy_arg.name for arg in + gather_source_arguments(int_g.source_kernels)) + target_arg_names = set(arg.loopy_arg.name for arg in + gather_arguments(int_g.source_kernels)) - source_arg_names args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( - int_g.kernel_arguments.items()) if k in int_g.get_args()) + int_g.kernel_arguments.items()) if k in target_arg_names) return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel, args) @@ -875,7 +798,8 @@ def merge_kernel_arguments(x, y): if k in res: if get_hashable_kernel_argument(res[k]) \ != get_hashable_kernel_argument(v): - raise ValueError + raise ValueError(f"Error merging values for {k}." + f"values were {res[k]} and {v}") else: res[k] = v return res From 4056a33b33b922c8d58b6fdf3fa1743028162f47 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 10 Oct 2021 17:00:39 -0500 Subject: [PATCH 193/209] handle directionaltargetderivative --- pytential/symbolic/pde/system_utils.py | 32 +++++++++++++++++--------- test/test_pde_system_utils.py | 19 ++++++++++++++- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 3cb4a53a9..37cdcf565 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -595,11 +595,9 @@ def get_int_g_source_group_identifier(int_g): """Return a identifier for a group for the *int_g* so that all elements in that group have the same source attributes. """ - from sumpy.tools import gather_source_arguments - source_arg_names = set(arg.loopy_arg.name for arg in - gather_source_arguments(int_g.source_kernels)) + target_arg_names = get_normal_vector_names(int_g.target_kernel) args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( - int_g.kernel_arguments.items()) if k in source_arg_names) + int_g.kernel_arguments.items()) if k not in target_arg_names) return (int_g.source, args, int_g.target_kernel.get_base_kernel()) @@ -607,16 +605,23 @@ def get_int_g_target_group_identifier(int_g): """Return a identifier for a group for the *int_g* so that all elements in that group have the same target attributes. """ - from sumpy.tools import gather_arguments, gather_source_arguments - source_arg_names = set(arg.loopy_arg.name for arg in - gather_source_arguments(int_g.source_kernels)) - target_arg_names = set(arg.loopy_arg.name for arg in - gather_arguments(int_g.source_kernels)) - source_arg_names + target_arg_names = get_normal_vector_names(int_g.target_kernel) args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( int_g.kernel_arguments.items()) if k in target_arg_names) return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel, args) +def get_normal_vector_names(kernel): + """Return the normal vector names in a kernel + """ + normal_vectors = set() + while isinstance(kernel, KernelWrapper): + if isinstance(kernel, DirectionalDerivative): + normal_vectors.add(kernel.dir_vec_name) + kernel = kernel.inner_kernel + return normal_vectors + + class IntGSubstitutor(IdentityMapper): """Replaces IntGs with pymbolic expression given by the replacements dictionary @@ -640,8 +645,12 @@ def remove_target_attributes(int_g): """Remove target attributes from *int_g* and return an expression that is common to all expression in the same source group. """ + normals = get_normal_vector_names(int_g.target_kernel) + kernel_arguments = {k: v for k, v in int_g.kernel_arguments.items() if + k not in normals} return int_g.copy(target=None, qbx_forced_limit=None, - target_kernel=int_g.target_kernel.get_base_kernel()) + target_kernel=int_g.target_kernel.get_base_kernel(), + kernel_arguments=kernel_arguments) def restore_target_attributes(expr, orig_int_g): @@ -655,7 +664,8 @@ def restore_target_attributes(expr, orig_int_g): int_g: int_g.copy(target=orig_int_g.target, qbx_forced_limit=orig_int_g.qbx_forced_limit, target_kernel=orig_int_g.target_kernel.replace_base_kernel( - int_g.target_kernel)) + int_g.target_kernel), + kernel_arguments=orig_int_g.kernel_arguments) for int_g in int_gs} substitutor = IntGSubstitutor(replacements) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 309e27377..93bb5b7bb 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -21,7 +21,8 @@ """ from sumpy.kernel import (LaplaceKernel, AxisSourceDerivative, - AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel, HelmholtzKernel) + AxisTargetDerivative, TargetPointMultiplier, BiharmonicKernel, HelmholtzKernel, + DirectionalTargetDerivative) from pytential.symbolic.primitives import (int_g_vec, D, IntG, NodeCoordinateComponent) from pytential.symbolic.pde.system_utils import (merge_int_g_exprs, @@ -234,6 +235,22 @@ def test_merge_directional_source(): assert result[0] == int_g3 +def test_merge_directional_target(): + from pymbolic.primitives import Variable + + dim = 3 + knl = DirectionalTargetDerivative(LaplaceKernel(dim), "target_dir") + density = Variable("density") + target_dir1 = make_sym_vector("target_dir1", dim) + target_dir2 = make_sym_vector("target_dir2", dim) + + int_g1 = int_g_vec(knl, density, qbx_forced_limit=1, target_dir=target_dir1) + int_g2 = int_g_vec(knl, density, qbx_forced_limit=1, target_dir=target_dir2) + + result = merge_int_g_exprs([int_g1 + int_g2]) + assert result[0] == int_g1 + int_g2 + + def test_restoring_target_attributes(): from pymbolic.primitives import Variable dim = 3 From f3a16605668ab759f9b864c0b38bf6fcededf1ed Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 11 Oct 2021 21:24:42 -0500 Subject: [PATCH 194/209] WIP: inner --- pytential/symbolic/pde/system_utils.py | 53 +++++++++++++++++++------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 37cdcf565..bd82ef6c4 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -359,6 +359,7 @@ def filter_kernel_arguments(knls, kernel_arguments): # }}} +# {{{ merge_int_g_exprs def merge_int_g_exprs(exprs, source_dependent_variables=None): """ @@ -398,19 +399,24 @@ def merge_int_g_exprs(exprs, source_dependent_variables=None): # Using a dictionary instead of a set because sets are unordered all_source_group_identifiers = {} - result = np.array([0 for _ in exprs], dtype=object) + result = [0]*len(exprs) int_g_cc = IntGCoefficientCollector() int_gs_by_source_group = defaultdict(list) - def add_int_gs_in_expr(expr): - for int_g in get_int_g_s([expr]): - source_group_identifier = get_int_g_source_group_identifier(int_g) - int_gs_by_source_group[source_group_identifier].append(int_g) - for density in int_g.densities: - add_int_gs_in_expr(density) + new_exprs = list(exprs) + + def add_int_gs_in_expr(int_g): + source_group_identifier = get_int_g_source_group_identifier(int_g) + int_gs_by_source_group[source_group_identifier].append(int_g) + for density in int_g.densities: + if have_int_g_s(density): + new_exprs.append(density) + result.append(0) - for i, expr in enumerate(exprs): + i = 0 + while i < len(new_exprs): + expr = new_exprs[i] int_gs_by_group = {} try: int_g_coeff_map = int_g_cc(expr) @@ -420,7 +426,9 @@ def add_int_gs_in_expr(expr): # some IntGs from them. logger.debug("%s is not linear", expr) result[i] += expr - add_int_gs_in_expr(expr) + for int_g in get_int_g_s([expr]): + add_int_gs_in_expr(expr) + i = i + 1 continue for int_g, coeff in int_g_coeff_map.items(): if int_g == 1: @@ -467,6 +475,8 @@ def add_int_gs_in_expr(expr): result[i] += result_int_g * coeff add_int_gs_in_expr(result_int_g) + i = i + 1 + # No IntGs found if all(not int_gs for int_gs in int_gs_by_source_group): return exprs @@ -519,8 +529,23 @@ def add_int_gs_in_expr(expr): for int_g in targetless_int_g_mapping[insn]: replacements[int_g] = restore_target_attributes(reduced_insn, int_g) - mapper = IntGSubstitutor(replacements) - result = [mapper(expr) for expr in result] + for r in result: + print("r", r) + + for k, v in replacements.items(): + print("outer_k", k) + print("outer_v", v) + outer_mapper = Substitutor(replacements) + + nexprs = len(exprs) + replacements_for_inner_exprs = {expr: outer_mapper(result_expr) for expr, result_expr in + zip(new_exprs[nexprs:], result[nexprs:])} + for k, v in replacements_for_inner_exprs.items(): + print("k", k) + print("v", v) + mapper = Substitutor(replacements_for_inner_exprs) + result = [outer_mapper(expr) for expr in result[:nexprs]] + orig_count = get_number_of_fmms(exprs) new_count = get_number_of_fmms(result) @@ -622,7 +647,7 @@ def get_normal_vector_names(kernel): return normal_vectors -class IntGSubstitutor(IdentityMapper): +class Substitutor(IdentityMapper): """Replaces IntGs with pymbolic expression given by the replacements dictionary """ @@ -668,7 +693,7 @@ def restore_target_attributes(expr, orig_int_g): kernel_arguments=orig_int_g.kernel_arguments) for int_g in int_gs} - substitutor = IntGSubstitutor(replacements) + substitutor = Substitutor(replacements) return substitutor(expr) @@ -832,6 +857,8 @@ def simplify_densities(densities): result.append(density) return tuple(result) +# }}} + if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) From 4d8bfb1a2f23b551c6d70ed8bb48097e4bda3964 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 11 Oct 2021 21:24:46 -0500 Subject: [PATCH 195/209] Revert "WIP: inner" This reverts commit f3a16605668ab759f9b864c0b38bf6fcededf1ed. --- pytential/symbolic/pde/system_utils.py | 53 +++++++------------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index bd82ef6c4..37cdcf565 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -359,7 +359,6 @@ def filter_kernel_arguments(knls, kernel_arguments): # }}} -# {{{ merge_int_g_exprs def merge_int_g_exprs(exprs, source_dependent_variables=None): """ @@ -399,24 +398,19 @@ def merge_int_g_exprs(exprs, source_dependent_variables=None): # Using a dictionary instead of a set because sets are unordered all_source_group_identifiers = {} - result = [0]*len(exprs) + result = np.array([0 for _ in exprs], dtype=object) int_g_cc = IntGCoefficientCollector() int_gs_by_source_group = defaultdict(list) - new_exprs = list(exprs) - - def add_int_gs_in_expr(int_g): - source_group_identifier = get_int_g_source_group_identifier(int_g) - int_gs_by_source_group[source_group_identifier].append(int_g) - for density in int_g.densities: - if have_int_g_s(density): - new_exprs.append(density) - result.append(0) + def add_int_gs_in_expr(expr): + for int_g in get_int_g_s([expr]): + source_group_identifier = get_int_g_source_group_identifier(int_g) + int_gs_by_source_group[source_group_identifier].append(int_g) + for density in int_g.densities: + add_int_gs_in_expr(density) - i = 0 - while i < len(new_exprs): - expr = new_exprs[i] + for i, expr in enumerate(exprs): int_gs_by_group = {} try: int_g_coeff_map = int_g_cc(expr) @@ -426,9 +420,7 @@ def add_int_gs_in_expr(int_g): # some IntGs from them. logger.debug("%s is not linear", expr) result[i] += expr - for int_g in get_int_g_s([expr]): - add_int_gs_in_expr(expr) - i = i + 1 + add_int_gs_in_expr(expr) continue for int_g, coeff in int_g_coeff_map.items(): if int_g == 1: @@ -475,8 +467,6 @@ def add_int_gs_in_expr(int_g): result[i] += result_int_g * coeff add_int_gs_in_expr(result_int_g) - i = i + 1 - # No IntGs found if all(not int_gs for int_gs in int_gs_by_source_group): return exprs @@ -529,23 +519,8 @@ def add_int_gs_in_expr(int_g): for int_g in targetless_int_g_mapping[insn]: replacements[int_g] = restore_target_attributes(reduced_insn, int_g) - for r in result: - print("r", r) - - for k, v in replacements.items(): - print("outer_k", k) - print("outer_v", v) - outer_mapper = Substitutor(replacements) - - nexprs = len(exprs) - replacements_for_inner_exprs = {expr: outer_mapper(result_expr) for expr, result_expr in - zip(new_exprs[nexprs:], result[nexprs:])} - for k, v in replacements_for_inner_exprs.items(): - print("k", k) - print("v", v) - mapper = Substitutor(replacements_for_inner_exprs) - result = [outer_mapper(expr) for expr in result[:nexprs]] - + mapper = IntGSubstitutor(replacements) + result = [mapper(expr) for expr in result] orig_count = get_number_of_fmms(exprs) new_count = get_number_of_fmms(result) @@ -647,7 +622,7 @@ def get_normal_vector_names(kernel): return normal_vectors -class Substitutor(IdentityMapper): +class IntGSubstitutor(IdentityMapper): """Replaces IntGs with pymbolic expression given by the replacements dictionary """ @@ -693,7 +668,7 @@ def restore_target_attributes(expr, orig_int_g): kernel_arguments=orig_int_g.kernel_arguments) for int_g in int_gs} - substitutor = Substitutor(replacements) + substitutor = IntGSubstitutor(replacements) return substitutor(expr) @@ -857,8 +832,6 @@ def simplify_densities(densities): result.append(density) return tuple(result) -# }}} - if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG) From 415953003e3f5a3a301304811c6949aae4271209 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 11 Oct 2021 22:03:02 -0500 Subject: [PATCH 196/209] Don't change mu, nu names --- pytential/symbolic/stokes.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 4d3614c48..f2d4dce12 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,7 +27,7 @@ from sumpy.kernel import (StressletKernel, LaplaceKernel, ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) -from pymbolic import var +from sumpy.symbolic import SpatialConstant from abc import ABC __doc__ = """ @@ -42,7 +42,7 @@ # {{{ StokesletWrapper/StressletWrapper ABCs -_MU_SYM_DEFAULT = var("mu") +_MU_SYM_DEFAULT = SpatialConstant("mu") class StokesletWrapperBase(ABC): @@ -267,9 +267,12 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): for deriv_dir in deriv_dirs: knl = AxisTargetDerivative(deriv_dir, knl) - args = [arg.loopy_arg.name for arg in knl.get_args()] - for arg in args: - kwargs[arg] = var(arg) + kernel_arg_names = set(karg.loopy_arg.name + for karg in (knl.get_args() + knl.get_source_args())) + + for var_name in ["mu", "nu"]: + if var_name not in kernel_arg_names: + kwargs.pop(var_name) res = sym.int_g_vec(knl, density, **kwargs) return res @@ -291,11 +294,14 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, raise ValueError("method has to be one of biharmonic/naive") self.kernel_dict = {} + # The two cases of nu=0.5 and nu!=0.5 differ significantly and + # ElasticityKernel needs to know if nu=0.5 or not at creation time + poisson_ratio = "nu" if nu_sym != 0.5 else 0.5 for i in range(dim): for j in range(i, dim): self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, - jcomp=j, viscosity_mu=mu_sym, poisson_ratio=nu_sym) + jcomp=j, poisson_ratio=poisson_ratio) # The dictionary allows us to exploit symmetry -- that # :math:`T_{01}` is identical to :math:`T_{10}` -- and avoid creating @@ -312,7 +318,8 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, """ res = _create_int_g(self.kernel_dict[idx], deriv_dirs, density=density_sym*dir_vec_sym[idx[-1]], - qbx_forced_limit=qbx_forced_limit)/(2*(1-self.nu)) + qbx_forced_limit=qbx_forced_limit, mu=self.mu, + nu=self.nu)/(2*(1-self.nu)) return res def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -330,7 +337,7 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): if self.base_kernel is None: return sym_expr else: - return rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel) + return np.array(rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel)) def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -389,7 +396,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, for j in range(i, dim): for k in range(j, dim): self.kernel_dict[(i, j, k)] = StressletKernel(dim=dim, icomp=i, - jcomp=j, kcomp=k, viscosity_mu=mu_sym) + jcomp=j, kcomp=k) # The dictionary allows us to exploit symmetry -- that # :math:`T_{012}` is identical to :math:`T_{120}` -- and avoid creating @@ -432,7 +439,7 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, knl = self.kernel_dict[kernel_idx] result += _create_int_g(knl, tuple(deriv_dirs) + tuple(extra_deriv_dirs), density=density_sym*dir_vec_sym[dir_vec_idx], - qbx_forced_limit=qbx_forced_limit) * coeff + qbx_forced_limit=qbx_forced_limit, mu=self.mu, nu=self.nu) * coeff return result/(2*(1 - nu)) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, @@ -450,7 +457,7 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, if self.base_kernel is None: return sym_expr else: - return rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel) + return np.array(rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel)) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, From 33143c453c9f2190732d051aba11c45c8acea6f7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 02:07:26 -0500 Subject: [PATCH 197/209] Fixes for change viscosity name --- pytential/symbolic/pde/system_utils.py | 68 +++++++++++++++++--------- pytential/symbolic/primitives.py | 19 +++---- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 37cdcf565..946475eba 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -35,7 +35,8 @@ from pymbolic.geometric_algebra.mapper import WalkMapper from pymbolic.mapper import CombineMapper from pymbolic.mapper.coefficient import CoefficientCollector -from pytential.symbolic.primitives import IntG, NodeCoordinateComponent +from pytential.symbolic.primitives import (IntG, NodeCoordinateComponent, + hashable_kernel_args, hashable_kernel_arg_value) from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand import pytential @@ -130,8 +131,18 @@ def convert_target_deriv_to_source(int_g): source_kernels=tuple(source_kernels)) +def _get_kernel_expression(expr, kernel_arguments): + from pymbolic.mapper.substitutor import substitute + from sumpy.symbolic import PymbolicToSympyMapperWithSymbols + + pymbolic_expr = substitute(expr, kernel_arguments) + + res = PymbolicToSympyMapperWithSymbols()(pymbolic_expr) + return res + + def convert_target_multiplier_to_source(int_g): - """Convert an IntG with TargetMultiplier to an sum of IntGs without + """Convert an IntG with TargetMultiplier to a sum of IntGs without TargetMultiplier and only source dependent transformations """ from sumpy.symbolic import SympyToPymbolicMapper @@ -141,7 +152,6 @@ def convert_target_multiplier_to_source(int_g): if isinstance(tgt_knl.inner_kernel, KernelWrapper): return [int_g] - new_kernel_args = filter_kernel_arguments([tgt_knl], int_g.kernel_arguments) result = [] # If the kernel is G, source is y and target is x, # x G = y*G + (x - y)*G @@ -149,21 +159,25 @@ def convert_target_multiplier_to_source(int_g): new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) for density in int_g.densities] result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, - densities=tuple(new_densities), kernel_arguments=new_kernel_args)) + densities=tuple(new_densities), + kernel_arguments=int_g.kernel_arguments)) # create a new expression kernel for (x - y)*G sym_d = make_sym_vector("d", tgt_knl.dim) + base_kernel_expr = _get_kernel_expression( + int_g.target_kernel.get_base_kernel().expression, + int_g.kernel_arguments) conv = SympyToPymbolicMapper() for knl, density in zip(int_g.source_kernels, int_g.densities): - new_expr = conv(knl.postprocess_at_source(knl.get_expression(sym_d), sym_d) + new_expr = conv(knl.postprocess_at_source(base_kernel_expr, sym_d) * sym_d[tgt_knl.axis]) new_knl = ExpressionKernel(knl.dim, new_expr, knl.get_base_kernel().global_scaling_const, knl.is_complex_valued) result.append(int_g.copy(target_kernel=new_knl, densities=(density,), - source_kernels=(new_knl,), kernel_arguments=new_kernel_args)) + source_kernels=(new_knl,))) return result @@ -183,7 +197,8 @@ def _convert_int_g_to_base(int_g, base_kernel): result = 0 for density, source_kernel in zip(int_g.densities, int_g.source_kernels): deriv_relation = get_deriv_relation_kernel(source_kernel.get_base_kernel(), - base_kernel) + base_kernel, hashable_kernel_arguments=( + hashable_kernel_args(int_g.kernel_arguments))) const = deriv_relation[0] # NOTE: we set a dofdesc here to force the evaluation of this integral @@ -219,21 +234,25 @@ def _convert_int_g_to_base(int_g, base_kernel): return result -def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None): +def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, + kernel_arguments=None): res = [] for knl in kernels: - res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order)) + res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order, + kernel_arguments=hashable_kernel_args(kernel_arguments))) return res @memoize_on_first_arg -def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None): +def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None, + hashable_kernel_arguments=None): + kernel_arguments = dict(hashable_kernel_arguments) (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order) dim = base_kernel.dim sym_vec = make_sym_vector("d", dim) sympy_conv = SympyToPymbolicMapper() - expr = kernel.get_expression(sym_vec) + expr = _get_kernel_expression(kernel.expression, kernel_arguments) vec = [] for i in range(len(mis)): vec.append(evalf(expr.xreplace(dict((k, v) for @@ -249,18 +268,21 @@ def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None): if coeff == 0: continue if mis[i] != (-1, -1, -1): - coeff *= kernel.get_global_scaling_const() - coeff /= base_kernel.get_global_scaling_const() + coeff *= _get_kernel_expression(kernel.global_scaling_const, + kernel_arguments) + coeff /= _get_kernel_expression(base_kernel.global_scaling_const, + kernel_arguments) result.append((mis[i], sympy_conv(coeff))) logger.debug(" + %s.diff(%s)*%s", base_kernel, mis[i], coeff) else: - const = sympy_conv(coeff * kernel.get_global_scaling_const()) + const = sympy_conv(coeff * _get_kernel_expression( + kernel.global_scaling_const, kernel_arguments)) logger.debug(" + %s", const) return (const, result) @memoize_on_first_arg -def _get_base_kernel_matrix(base_kernel, order=None, retries=3): +def _get_base_kernel_matrix(base_kernel, order=None, retries=3, kernel_arguments=None): dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() @@ -288,7 +310,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3): rand[i, j] = sym.sympify(rand[i, j])/10**15 sym_vec = make_sym_vector("d", dim) - base_expr = base_kernel.get_expression(sym_vec) + base_expr = _get_kernel_expression(base_kernel.expression, kernel_arguments) mat = [] for rand_vec_idx in range(rand.shape[1]): @@ -596,9 +618,10 @@ def get_int_g_source_group_identifier(int_g): group have the same source attributes. """ target_arg_names = get_normal_vector_names(int_g.target_kernel) - args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( + args = dict((k, v) for k, v in sorted( int_g.kernel_arguments.items()) if k not in target_arg_names) - return (int_g.source, args, int_g.target_kernel.get_base_kernel()) + return (int_g.source, hashable_kernel_args(args), + int_g.target_kernel.get_base_kernel()) def get_int_g_target_group_identifier(int_g): @@ -606,9 +629,10 @@ def get_int_g_target_group_identifier(int_g): group have the same target attributes. """ target_arg_names = get_normal_vector_names(int_g.target_kernel) - args = tuple((k, get_hashable_kernel_argument(v)) for k, v in sorted( + args = dict((k, v) for k, v in sorted( int_g.kernel_arguments.items()) if k in target_arg_names) - return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel, args) + return (int_g.target, int_g.qbx_forced_limit, int_g.target_kernel, + hashable_kernel_args(args)) def get_normal_vector_names(kernel): @@ -806,8 +830,8 @@ def merge_kernel_arguments(x, y): res = x.copy() for k, v in y.items(): if k in res: - if get_hashable_kernel_argument(res[k]) \ - != get_hashable_kernel_argument(v): + if hashable_kernel_arg_value(res[k]) \ + != hashable_kernel_arg_value(v): raise ValueError(f"Error merging values for {k}." f"values were {res[k]} and {v}") else: diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 5e046f9c5..99e75de14 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1345,14 +1345,15 @@ def laplace(ambient_dim, operand): # {{{ potentials -def hashable_kernel_args(kernel_arguments): - hashable_args = [] - for key, val in sorted(kernel_arguments.items()): - if isinstance(val, np.ndarray): - val = tuple(val) - hashable_args.append((key, val)) +def hashable_kernel_arg_value(val): + if isinstance(val, np.ndarray): + val = tuple(val) + return val + - return tuple(hashable_args) +def hashable_kernel_args(kernel_arguments): + return tuple([(key, hashable_kernel_arg_value(val)) for key, val in \ + sorted(kernel_arguments.items())]) class IntG(Expression): @@ -1438,8 +1439,8 @@ def __init__(self, target_kernel, source_kernels, densities, knl_density_dict[source_kernel] += density else: knl_density_dict[source_kernel] = density - knl_density_dict = OrderedDict((k, v) for k, v in - knl_density_dict.items() if v) + knl_density_dict = OrderedDict( + [(k, v) for k, v in knl_density_dict.items() if v]) densities = tuple(knl_density_dict.values()) source_kernels = tuple(knl_density_dict.keys()) From bddf4dc0b7d556316ef76e1586bc420d5dffbf8e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 02:10:17 -0500 Subject: [PATCH 198/209] change viscosity name in test to see if it works --- test/test_stokes.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 1db756bca..883531c6c 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -30,6 +30,7 @@ from meshmode.discretization.poly_element import \ InterpolatoryQuadratureSimplexGroupFactory from pytools.obj_array import make_obj_array +from sumpy.symbolic import SpatialConstant from meshmode import _acf # noqa: F401 from arraycontext import pytest_generate_tests_for_array_contexts @@ -146,12 +147,12 @@ def run_exterior_stokes(actx_factory, *, # {{{ symbolic sym_normal = sym.make_sym_vector("normal", ambient_dim) - sym_mu = sym.var("mu") + sym_mu = SpatialConstant("mu2") if nu == 0.5: sym_nu = 0.5 else: - sym_nu = sym.var("nu") + sym_nu = SpatialConstant("nu2") if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator @@ -201,17 +202,17 @@ def run_exterior_stokes(actx_factory, *, omega = bind(places, total_charge * sym.Ones())(actx) if ambient_dim == 2: - bc_context = {"mu": mu, "omega": omega} - op_context = {"mu": mu, "omega": omega, "normal": normal} + bc_context = {"mu2": mu, "omega": omega} + op_context = {"mu2": mu, "omega": omega, "normal": normal} else: bc_context = {} - op_context = {"mu": mu, "normal": normal} - direct_context = {"mu": mu} + op_context = {"mu2": mu, "normal": normal} + direct_context = {"mu2": mu} if sym_nu != 0.5: - bc_context["nu"] = nu - op_context["nu"] = nu - direct_context["nu"] = nu + bc_context["nu2"] = nu + op_context["nu2"] = nu + direct_context["nu2"] = nu bc_op = bind(places, sym_source_pot, auto_where=("point_source", "source")) From f93c494b84e5d54efb888e946e04f2a9b8220460 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 02:13:58 -0500 Subject: [PATCH 199/209] point sumpy to spatial constant branch --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4c26b26a5..88c7a5eb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,5 +9,5 @@ git+https://github.com/inducer/loopy.git#egg=loopy git+https://github.com/inducer/boxtree.git#egg=boxtree git+https://github.com/inducer/arraycontext.git#egg=arraycontext git+https://github.com/inducer/meshmode.git#egg=meshmode -git+https://github.com/inducer/sumpy.git#egg=sumpy +git+https://github.com/isuruf/sumpy.git@spatial_constant#egg=sumpy git+https://github.com/inducer/pyfmmlib.git#egg=pyfmmlib From 0928d792191b1997986a213bb5323beaadaf3742 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 02:36:09 -0500 Subject: [PATCH 200/209] make rewrite_using_base_kernel accept None as base_kernel --- pytential/symbolic/pde/system_utils.py | 7 ++++++- pytential/symbolic/stokes.py | 12 ++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 946475eba..cc59362e0 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -60,7 +60,10 @@ # {{{ rewrite_using_base_kernel -def rewrite_using_base_kernel(exprs, base_kernel): +_NO_ARG_SENTINEL = object() + + +def rewrite_using_base_kernel(exprs, base_kernel=_NO_ARG_SENTINEL): """Rewrites an expression with :class:`~pytential.symbolic.primitives.IntG` objects using *base_kernel*. @@ -70,6 +73,8 @@ def rewrite_using_base_kernel(exprs, base_kernel): The routine will fail if this process cannot be completed. """ + if base_kernel is None: + return list(exprs) mapper = RewriteUsingBaseKernelMapper(base_kernel) return [mapper(expr) for expr in exprs] diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index f2d4dce12..c47139189 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -334,10 +334,8 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): density_vec_sym[i], [1]*self.dim, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - if self.base_kernel is None: - return sym_expr - else: - return np.array(rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel)) + return np.array(rewrite_using_base_kernel(sym_expr, + base_kernel=self.base_kernel)) def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -454,10 +452,8 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, density_vec_sym[i], dir_vec_sym, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - if self.base_kernel is None: - return sym_expr - else: - return np.array(rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel)) + return np.array(rewrite_using_base_kernel(sym_expr, + base_kernel=self.base_kernel)) def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, From 1e25c86e9eb2c8100f3112e64b3b6fcc2b567b5d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 02:36:29 -0500 Subject: [PATCH 201/209] Fix formatting --- pytential/symbolic/pde/system_utils.py | 3 ++- pytential/symbolic/primitives.py | 2 +- pytential/symbolic/stokes.py | 5 ++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index cc59362e0..9f82232d7 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -287,7 +287,8 @@ def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None, @memoize_on_first_arg -def _get_base_kernel_matrix(base_kernel, order=None, retries=3, kernel_arguments=None): +def _get_base_kernel_matrix(base_kernel, order=None, retries=3, + kernel_arguments=None): dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 99e75de14..abc60d834 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1352,7 +1352,7 @@ def hashable_kernel_arg_value(val): def hashable_kernel_args(kernel_arguments): - return tuple([(key, hashable_kernel_arg_value(val)) for key, val in \ + return tuple([(key, hashable_kernel_arg_value(val)) for key, val in sorted(kernel_arguments.items())]) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index c47139189..a2cdd556c 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -270,6 +270,8 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): kernel_arg_names = set(karg.loopy_arg.name for karg in (knl.get_args() + knl.get_source_args())) + # When the kernel is Laplace, mu and nu are not kernel arguments + # Also when nu==0.5, it's not a kernel argument to StokesletKernel for var_name in ["mu", "nu"]: if var_name not in kernel_arg_names: kwargs.pop(var_name) @@ -437,7 +439,8 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, knl = self.kernel_dict[kernel_idx] result += _create_int_g(knl, tuple(deriv_dirs) + tuple(extra_deriv_dirs), density=density_sym*dir_vec_sym[dir_vec_idx], - qbx_forced_limit=qbx_forced_limit, mu=self.mu, nu=self.nu) * coeff + qbx_forced_limit=qbx_forced_limit, mu=self.mu, nu=self.nu) * \ + coeff return result/(2*(1 - nu)) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, From e3498039b91559204ef8ad19284acb7b626de61c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 08:47:31 -0500 Subject: [PATCH 202/209] sympy 1.9 compatibility --- pytential/symbolic/pde/reduce_fmms.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index a232fed04..559000e38 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -115,19 +115,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): if right_factor.shape[0] >= mat.shape[0]: return int_gs - # cast to SymPy Polynomial objects - right_factor = right_factor.applyfunc(lambda x: x.as_poly(*axis_vars, - domain=sympy.EX)) - left_factor = left_factor.applyfunc(lambda x: x.as_poly(*axis_vars, - domain=sympy.EX)) - base_kernel = int_gs[0].source_kernels[0].get_base_kernel() base_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) # Convert polynomials back to IntGs with source derivatives - source_int_gs = [[_convert_source_poly_to_int_g_derivs(poly, base_int_g, - axis_vars) for poly in row] for row in right_factor.tolist()] + source_int_gs = [[_convert_source_poly_to_int_g_derivs( + expr.as_poly(*axis_vars, domain=sympy.EX), base_int_g, + axis_vars) for expr in row] for row in right_factor.tolist()] # For each row in the right factor, merge the IntGs to one IntG # to get a total of k IntGs. @@ -148,7 +143,8 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): res = [0]*left_factor.shape[0] for i in range(left_factor.shape[0]): for j in range(left_factor.shape[1]): - res[i] += _convert_target_poly_to_int_g_derivs(left_factor[i, j], + res[i] += _convert_target_poly_to_int_g_derivs( + left_factor[i, j].as_poly(*axis_vars, domain=sympy.EX), int_gs[i], source_int_gs_merged[j]) return res From 979f8f46525a826db18e5ef3e79b1aa6242f83ea Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 08:48:57 -0500 Subject: [PATCH 203/209] fix formatting --- pytential/symbolic/pde/reduce_fmms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/reduce_fmms.py b/pytential/symbolic/pde/reduce_fmms.py index 559000e38..6bfab4f3e 100644 --- a/pytential/symbolic/pde/reduce_fmms.py +++ b/pytential/symbolic/pde/reduce_fmms.py @@ -122,7 +122,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): # Convert polynomials back to IntGs with source derivatives source_int_gs = [[_convert_source_poly_to_int_g_derivs( expr.as_poly(*axis_vars, domain=sympy.EX), base_int_g, - axis_vars) for expr in row] for row in right_factor.tolist()] + axis_vars) for expr in row] for row in right_factor.tolist()] # For each row in the right factor, merge the IntGs to one IntG # to get a total of k IntGs. From 4d4fceb81f718f0764e183241a486f038a824269 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 09:19:04 -0500 Subject: [PATCH 204/209] Fix pylint lints --- pytential/symbolic/pde/system_utils.py | 14 +------------- pytential/symbolic/stokes.py | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 9f82232d7..2b0d7693c 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -244,7 +244,7 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, res = [] for knl in kernels: res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order, - kernel_arguments=hashable_kernel_args(kernel_arguments))) + hashable_kernel_arguments=hashable_kernel_args(kernel_arguments))) return res @@ -880,16 +880,4 @@ def simplify_densities(densities): expression_knl2 = ExpressionKernel(3, conv(1/sym_r + sym_d[0]*sym_d[0]/sym_r**3), 1, False) kernels = [expression_knl, expression_knl2] - #kernels += [ElasticityKernel(3, 0, 1, poisson_ratio="0.4"), - # ElasticityKernel(3, 0, 0, poisson_ratio="0.4")] get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4) - density = pytential.sym.make_sym_vector("d", 1)[0] - from pytential.symbolic.primitives import int_g_vec - int_g_1 = int_g_vec(TargetPointMultiplier(2, AxisTargetDerivative(2, - AxisSourceDerivative(1, AxisSourceDerivative(0, - LaplaceKernel(3))))), density, qbx_forced_limit=1) - int_g_2 = int_g_vec(TargetPointMultiplier(0, AxisTargetDerivative(0, - AxisSourceDerivative(0, AxisSourceDerivative(0, - LaplaceKernel(3))))), density, qbx_forced_limit=1) - print(merge_int_g_exprs([int_g_1, int_g_2], - base_kernel=BiharmonicKernel(3))[0]) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index a2cdd556c..c325dd3a0 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -350,10 +350,13 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): for comp in range(self.dim): for i in range(self.dim): for j in range(self.dim): + # pylint does not like __new__ returning new object + # pylint: disable=no-member sym_expr[comp] += dir_vec_sym[i] * \ stresslet_obj.get_int_g((comp, i, j), density_vec_sym[j], [1]*self.dim, qbx_forced_limit, deriv_dirs=[]) + # pylint: enable=no-member return sym_expr @@ -803,10 +806,14 @@ def __init__(self, *, omega, alpha=1.0, eta=1.0, method="biharmonic", def _farfield(self, qbx_forced_limit): source_dofdesc = sym.DOFDescriptor(None, discr_stage=sym.QBX_SOURCE_STAGE1) length = sym.integral(self.ambient_dim, self.dim, 1, dofdesc=source_dofdesc) - return self.stresslet.apply_stokeslet_and_stresslet( + # pylint does not like __new__ returning new object + # pylint: disable=no-member + result = self.stresslet.apply_stokeslet_and_stresslet( -self.omega / length, [0]*self.ambient_dim, [0]*self.ambient_dim, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=1, stresslet_weight=0) + # pylint: enable=no-member + return result def _operator(self, sigma, normal, qbx_forced_limit): slp_qbx_forced_limit = qbx_forced_limit @@ -823,9 +830,12 @@ def _operator(self, sigma, normal, qbx_forced_limit): self.dim, sigma, dofdesc=dd)) result = self.eta * self.alpha / (2.0 * np.pi) * int_sigma + # pylint does not like __new__ returning new object + # pylint: disable=no-member result += self.stresslet.apply_stokeslet_and_stresslet(meanless_sigma, sigma, normal, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=-self.eta, stresslet_weight=1) + # pylint: enable=no-member return result @@ -879,10 +889,14 @@ def __init__(self, *, eta=None, method="laplace", mu_sym=_MU_SYM_DEFAULT, self.laplace_kernel = LaplaceKernel(3) def _operator(self, sigma, normal, qbx_forced_limit): - return self.stresslet.apply_stokeslet_and_stresslet(sigma, + # pylint does not like __new__ returning new object + # pylint: disable=no-member + result = self.stresslet.apply_stokeslet_and_stresslet(sigma, sigma, normal, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=self.eta, stresslet_weight=1, extra_deriv_dirs=()) + # pylint: enable=no-member + return result def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # NOTE: H. 1986 Equation 17 From 5f7904d55561c9246c929bd75afb652a1ccf1509 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 13 Oct 2021 21:34:51 -0500 Subject: [PATCH 205/209] use sympy/sympy#22273 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 88c7a5eb2..b599210ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy git+git://github.com/inducer/pytools.git#egg=pytools git+git://github.com/inducer/pymbolic.git#egg=pymbolic -sympy +git+https://github.com/isuruf/sympy.git@syzygies#egg=sympy git+https://github.com/inducer/modepy.git#egg=modepy git+https://github.com/inducer/pyopencl.git#egg=pyopencl git+https://github.com/inducer/islpy.git#egg=islpy From 103a88994b515af1483440f06d2bf4b8dd25d0b5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 15 Nov 2021 08:47:35 -0600 Subject: [PATCH 206/209] Support AxisTargetDerivative(TargetPointMultiplier) --- pytential/symbolic/pde/system_utils.py | 112 ++++++++++++++++++++----- test/test_pde_system_utils.py | 8 +- 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 2b0d7693c..2d784612b 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -125,7 +125,7 @@ def convert_target_deriv_to_source(int_g): # TargetPointMultiplier has to be the outermost kernel # If it is the inner kernel, return early if isinstance(knl, TargetPointMultiplier): - return 1, int_g + return int_g for axis in reversed(multipliers): knl = TargetPointMultiplier(axis, knl) @@ -146,42 +146,108 @@ def _get_kernel_expression(expr, kernel_arguments): return res +def _monom_to_expr(monom, variables): + prod = 1 + for i, nrepeats in enumerate(monom): + for _ in range(nrepeats): + prod *= variables[i] + return prod + + def convert_target_multiplier_to_source(int_g): """Convert an IntG with TargetMultiplier to a sum of IntGs without TargetMultiplier and only source dependent transformations """ + import sympy + import sumpy.symbolic as sym from sumpy.symbolic import SympyToPymbolicMapper - tgt_knl = int_g.target_kernel - if not isinstance(tgt_knl, TargetPointMultiplier): - return [int_g] - if isinstance(tgt_knl.inner_kernel, KernelWrapper): + conv = SympyToPymbolicMapper() + + knl = int_g.target_kernel + # we use a symbol for d = (x - y) + ds = sympy.symbols(f"d0:{knl.dim}") + sources = sympy.symbols(f"y0:{knl.dim}") + # instead of just x, we use x = (d + y) + targets = [d + source for d, source in zip(ds, sources)] + orig_expr = sympy.Function("f")(*ds) + expr = orig_expr + found = False + while isinstance(knl, KernelWrapper): + if isinstance(knl, TargetPointMultiplier): + expr = targets[knl.axis] * expr + found = True + elif isinstance(knl, AxisTargetDerivative): + # sympy can't differentiate w.r.t target because + # it's not a symbol, but d/d(x) = d/d(d) + expr = expr.diff(ds[knl.axis]) + else: + return [int_g] + knl = knl.inner_kernel + + if not found: return [int_g] + sources_pymbolic = [NodeCoordinateComponent(i) for i in range(knl.dim)] + expr = expr.expand() + # Now the expr is an Add and looks like + # u''(d, s)*d[0] + u(d, s) + assert isinstance(expr, sympy.Add) result = [] - # If the kernel is G, source is y and target is x, - # x G = y*G + (x - y)*G - # For y*G, absorb y into a density - new_densities = [density*NodeCoordinateComponent(tgt_knl.axis) - for density in int_g.densities] - result.append(int_g.copy(target_kernel=tgt_knl.inner_kernel, - densities=tuple(new_densities), - kernel_arguments=int_g.kernel_arguments)) - - # create a new expression kernel for (x - y)*G - sym_d = make_sym_vector("d", tgt_knl.dim) - base_kernel_expr = _get_kernel_expression( - int_g.target_kernel.get_base_kernel().expression, + + for arg in expr.args: + deriv_terms = arg.atoms(sympy.Derivative) + if len(deriv_terms) == 1: + deriv_term = deriv_terms.pop() + rest_terms = sympy.Poly(arg.xreplace({deriv_term: 1}), *ds, *sources) + derivatives = deriv_term.args[1:] + elif len(deriv_terms) == 0: + rest_terms = sympy.Poly(arg.xreplace({orig_expr: 1}), *ds, *sources) + derivatives = [(d, 0) for d in ds] + else: + assert False, "impossible condition" + assert len(rest_terms.terms()) == 1 + monom, coeff = rest_terms.terms()[0] + expr_multiplier = _monom_to_expr(monom[:len(ds)], ds) + density_multiplier = _monom_to_expr(monom[len(ds):], sources_pymbolic) \ + * conv(coeff) + + new_int_gs = _multiply_int_g(int_g, sym.sympify(expr_multiplier), + density_multiplier) + for new_int_g in new_int_gs: + knl = new_int_g.target_kernel + for axis_var, nrepeats in derivatives: + axis = ds.index(axis_var) + for _ in range(nrepeats): + knl = AxisTargetDerivative(axis, knl) + result.append(new_int_g.copy(target_kernel=knl)) + return result + + +def _multiply_int_g(int_g, expr_multiplier, density_multiplier): + """Multiply the exprssion in IntG with the *expr_multiplier* + which is a symbolic expression and multiply the densities + with *density_multiplier* which is a pymbolic expression. + """ + from sumpy.symbolic import SympyToPymbolicMapper + result = [] + + base_kernel = int_g.target_kernel.get_base_kernel() + sym_d = make_sym_vector("d", base_kernel.dim) + base_kernel_expr = _get_kernel_expression(base_kernel.expression, int_g.kernel_arguments) conv = SympyToPymbolicMapper() for knl, density in zip(int_g.source_kernels, int_g.densities): - new_expr = conv(knl.postprocess_at_source(base_kernel_expr, sym_d) - * sym_d[tgt_knl.axis]) - new_knl = ExpressionKernel(knl.dim, new_expr, + if expr_multiplier == 1: + new_knl = knl.get_base_kernel() + else: + new_expr = conv(knl.postprocess_at_source(base_kernel_expr, sym_d) + * expr_multiplier) + new_knl = ExpressionKernel(knl.dim, new_expr, knl.get_base_kernel().global_scaling_const, knl.is_complex_valued) result.append(int_g.copy(target_kernel=new_knl, - densities=(density,), + densities=(density*density_multiplier,), source_kernels=(new_knl,))) return result @@ -235,7 +301,7 @@ def _convert_int_g_to_base(int_g, base_kernel): knl = AxisSourceDerivative(d, knl) c *= -1 result += int_g.copy(source_kernels=(knl,), target_kernel=target_kernel, - densities=(density,), kernel_arguments=new_kernel_args) * c + densities=(density * c,), kernel_arguments=new_kernel_args) return result diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 93bb5b7bb..d78176f00 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -139,12 +139,12 @@ def test_base_kernel_merge(): for i in range(dim)])) int_g3 = IntG(target_kernel=biharm_knl, - source_kernels=[AxisSourceDerivative(0, biharm_knl)] + source_kernels, - densities=[2*density] + [density*sources[0]*(-1.0) for _ in range(dim)], + source_kernels=source_kernels + [AxisSourceDerivative(0, biharm_knl)], + densities=[density*sources[0]*(-1.0) for _ in range(dim)] + [2*density], qbx_forced_limit=1) int_g4 = IntG(target_kernel=biharm_knl, - source_kernels=[AxisSourceDerivative(1, biharm_knl)] + source_kernels, - densities=[2*density] + [density*sources[1]*(-1.0) for _ in range(dim)], + source_kernels=source_kernels + [AxisSourceDerivative(1, biharm_knl)], + densities=[density*sources[1]*(-1.0) for _ in range(dim)] + [2*density], qbx_forced_limit=1) assert result[0] == int_g3 From 09da9ad3800bf80584c4761fefc2d3d086c2540e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Jun 2022 14:13:23 -0500 Subject: [PATCH 207/209] Fix lint --- pytential/symbolic/pde/system_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 2d784612b..afcf850a5 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -204,7 +204,7 @@ def convert_target_multiplier_to_source(int_g): rest_terms = sympy.Poly(arg.xreplace({orig_expr: 1}), *ds, *sources) derivatives = [(d, 0) for d in ds] else: - assert False, "impossible condition" + raise AssertionError("impossible condition") assert len(rest_terms.terms()) == 1 monom, coeff = rest_terms.terms()[0] expr_multiplier = _monom_to_expr(monom[:len(ds)], ds) From d81e16c822adefb69605df20f1e1caf87c43ddf0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Jun 2022 17:51:29 -0500 Subject: [PATCH 208/209] Fix pylint --- pytential/symbolic/pde/system_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index afcf850a5..8891db22d 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -169,7 +169,8 @@ def convert_target_multiplier_to_source(int_g): sources = sympy.symbols(f"y0:{knl.dim}") # instead of just x, we use x = (d + y) targets = [d + source for d, source in zip(ds, sources)] - orig_expr = sympy.Function("f")(*ds) + f = sympy.Function("f") + orig_expr = f(*ds) expr = orig_expr found = False while isinstance(knl, KernelWrapper): From feec35e928767dd6add0cba55883cd15bed93445 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 27 Jun 2022 09:30:19 -0500 Subject: [PATCH 209/209] Reduce order expectation for new tests --- test/test_stokes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 9d3a87b3f..5d342c5aa 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -356,12 +356,17 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) error_format="%.8e", eoc_format="%.2f")) + extra_order = 0 + if method == "biharmonic": + extra_order += 2 + elif nu != 0.5: + extra_order += 0.5 for eoc in eocs: # This convergence data is not as clean as it could be. See # https://github.com/inducer/pytential/pull/32 # for some discussion. order = min(target_order, qbx_order) - assert eoc.order_estimate() > order - 0.5 + assert eoc.order_estimate() > order - 0.5 - extra_order # }}}