From 65d126c3374d5fd097633e388ccd2092c18e925a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 27 Jun 2022 09:39:39 -0500 Subject: [PATCH 001/156] Stokes/Elasticity using biharmonic/Laplace --- pytential/symbolic/elasticity.py | 166 +++++ pytential/symbolic/pde/system_utils.py | 466 +++++++++++++ pytential/symbolic/stokes.py | 873 +++++++++++++++---------- pytential/utils.py | 61 +- requirements.txt | 2 +- test/test_stokes.py | 116 +++- 6 files changed, 1318 insertions(+), 366 deletions(-) create mode 100644 pytential/symbolic/elasticity.py create mode 100644 pytential/symbolic/pde/system_utils.py diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py new file mode 100644 index 000000000..64e6e766b --- /dev/null +++ b/pytential/symbolic/elasticity.py @@ -0,0 +1,166 @@ +__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 sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, + TargetPointMultiplier, LaplaceKernel) +from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase, + _MU_SYM_DEFAULT) + + +class StressletWrapperYoshida(StressletWrapperBase): + """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 __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 " + "StressletWrapperYoshida") + 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=()): + 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=()): + + 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 + 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 * 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))) + 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=add_extra_deriv_dirs( + AxisTargetDerivative(i, int_g.target_kernel))) + return res / (4*mu*(1 - nu)) + + sym_expr = np.zeros((3,), dtype=object) + + kernel = self.kernel + source = [sym.NodeCoordinateComponent(d) for d in range(3)] + normal = dir_vec_sym + sigma = stresslet_density_vec_sym + + source_kernels = [None]*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): + densities = [0]*4 + for l in range(3): # noqa: E741 + 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) + + densities = [0]*4 + 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] + 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), + qbx_forced_limit=qbx_forced_limit) + 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=_MU_SYM_DEFAULT, 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, 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, + extra_deriv_dirs) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py new file mode 100644 index 000000000..061c3e2e8 --- /dev/null +++ b/pytential/symbolic/pde/system_utils.py @@ -0,0 +1,466 @@ +__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 make_sym_vector, SympyToPymbolicMapper +import sumpy.symbolic as sym +from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, + ExpressionKernel, KernelWrapper, TargetPointMultiplier) +from pytools import (memoize_on_first_arg, + generate_nonnegative_integer_tuples_summing_to_at_most + as gnitstam) + +from pytential.symbolic.primitives import (NodeCoordinateComponent, + hashable_kernel_args) +from pytential.symbolic.mappers import IdentityMapper +from pytential.utils import chop, lu_solve_with_expand +import pytential + +import logging +logger = logging.getLogger(__name__) + +__all__ = ( + "rewrite_using_base_kernel", + "get_deriv_relation", + ) + +__doc__ = """ +.. autofunction:: rewrite_using_base_kernel +.. autofunction:: get_deriv_relation +""" + + +# {{{ rewrite_using_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*. + + 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. + """ + if base_kernel is None: + return list(exprs) + 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 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 _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 _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 + 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) # pylint: disable=not-callable + 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 = [] + + 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: + 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) + 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): + 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*density_multiplier,), + source_kernels=(new_knl,))) + 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, 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 + # 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 * c,), kernel_arguments=new_kernel_args) + return result + + +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, + hashable_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, + 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 = _get_kernel_expression(kernel.expression, kernel_arguments) + 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 *= _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 * _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, + kernel_arguments=None): + 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 = _get_kernel_expression(base_kernel.expression, kernel_arguments) + + 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} + +# }}} + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + 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)] + 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 = 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), + 1, False) + kernels = [expression_knl, expression_knl2] + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index cfe8bef3c..c325dd3a0 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -23,7 +23,12 @@ import numpy as np from pytential import sym -from sumpy.kernel import StokesletKernel, StressletKernel, LaplaceKernel +from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel +from sumpy.kernel import (StressletKernel, LaplaceKernel, + ElasticityKernel, BiharmonicKernel, + AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) +from sumpy.symbolic import SpatialConstant +from abc import ABC __doc__ = """ .. autoclass:: StokesletWrapper @@ -35,9 +40,12 @@ """ -# {{{ StokesletWrapper +# {{{ StokesletWrapper/StressletWrapper ABCs -class StokesletWrapper: +_MU_SYM_DEFAULT = SpatialConstant("mu") + + +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 @@ -59,43 +67,17 @@ class StokesletWrapper: :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 """ - - def __init__(self, dim=None): + def __init__(self, dim, mu_sym, nu_sym): self.dim = dim + self.mu = mu_sym + self.nu = nu_sym - 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) - } - - 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) - } - - else: - raise ValueError("unsupported dimension given to StokesletWrapper") - - def apply(self, density_vec_sym, mu_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 @@ -103,58 +85,30 @@ 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`. + :arg extra_deriv_dirs: adds target derivatives to all the integral + objects with the given derivative axis. """ + raise NotImplementedError - sym_expr = np.empty((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.int32) - 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.int_g_vec( - 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.int_g_vec( - self.kernel_dict[ctr_key], density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, mu=mu_sym) - - return sym_expr - - 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.""" + # 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 for i in range(self.dim): - - if i < 1: - sym_expr = DerivativeTaker(i).map_int_g( - sym.int_g_vec(kernel, density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit)) - else: - sym_expr = sym_expr + (DerivativeTaker(i).map_int_g( - sym.int_g_vec(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 - 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 @@ -163,46 +117,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`. """ + return self.apply(density_vec_sym, qbx_forced_limit, [deriv_dir]) - from pytential.symbolic.mappers import DerivativeTaker - - sym_expr = np.empty((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.int32) - 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.int_g_vec(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.int_g_vec(self.kernel_dict[ctr_key], - density_vec_sym[i], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym)) - - 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): r"""Symbolic expression for viscous stress applied to a direction. Returns a vector of symbolic expressions for the force resulting @@ -222,50 +142,13 @@ 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`. """ + raise NotImplementedError - import itertools - - sym_expr = np.empty((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.int32) - 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] = dir_vec_sym[i] * sym.int_g_vec( - 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.int_g_vec( - stresslet_obj.kernel_dict[ctr_key], - density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym) - - return sym_expr - -# }}} - - -# {{{ StressletWrapper -class StressletWrapper: +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 @@ -286,48 +169,18 @@ class StressletWrapper: :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 .. automethod:: apply_derivative .. automethod:: apply_stress """ - - def __init__(self, dim=None): + def __init__(self, dim, mu_sym, nu_sym): self.dim = dim + self.mu = mu_sym + self.nu = nu_sym - 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") - - 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, + extra_deriv_dirs=()): """Symbolic expressions for integrating Stresslet kernel. Returns an object array of symbolic expressions for the vector @@ -336,124 +189,55 @@ 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`. + :arg extra_deriv_dirs: adds target derivatives to all the integral + objects with the given derivative axis. """ + raise NotImplementedError - import itertools - - sym_expr = np.empty((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.int32) - 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.int_g_vec( - 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.int_g_vec( - self.kernel_dict[ctr_key], - dir_vec_sym[i] * density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym) - - 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.""" + def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + """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) - factor = (2. * mu_sym) + factor = (2. * self.mu) - 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.int_g_vec(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.int_g_vec(kernel, density_vec_sym[i] * dir_vec_sym[j], - qbx_forced_limit=qbx_forced_limit)))) + 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): - """Symbolic derivative of velocity from stresslet. + 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 Stresslet kernel with variable *density_vec_sym* and source - direction vectors *dir_vec_sym*. + 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 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`. """ - - import itertools - from pytential.symbolic.mappers import DerivativeTaker - - sym_expr = np.empty((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.int32) - 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.int_g_vec(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.int_g_vec(self.kernel_dict[ctr_key], - dir_vec_sym[i] * density_vec_sym[j], - qbx_forced_limit=qbx_forced_limit, - mu=mu_sym)) - - return sym_expr + 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, - 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 @@ -469,10 +253,234 @@ 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`. """ + raise NotImplementedError + +# }}} + + +# {{{ StokesletWrapper Naive and Biharmonic impl + +def _create_int_g(knl, deriv_dirs, density, **kwargs): + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + + 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) + + res = sym.int_g_vec(knl, density, **kwargs) + return res + + +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) + 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 = {} + # 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, poisson_ratio=poisson_ratio) + + # 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)] + + 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. + """ + res = _create_int_g(self.kernel_dict[idx], deriv_dirs, + density=density_sym*dir_vec_sym[idx[-1]], + 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=()): + + 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], [1]*self.dim, + qbx_forced_limit, deriv_dirs=extra_deriv_dirs) + + 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): + + 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. + 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 + + +class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + 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): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + method="biharmonic") + + +# }}} + + +# {{{ StressletWrapper Naive and Biharmonic impl + +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) + 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): + 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) + + # 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): + 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] + + # For elasticity (nu != 0.5), we need the LaplaceKernel + self.kernel_dict["laplace"] = LaplaceKernel(self.dim) + + 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. + """ + + 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)/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 + + 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, 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 + return result/(2*(1 - nu)) + + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=()): + + 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=extra_deriv_dirs) + + 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, + 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, + qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight + + return sym_expr + + def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, + qbx_forced_limit): sym_expr = np.empty((self.dim,), dtype=object) @@ -480,25 +488,208 @@ 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]) ) return sym_expr + +class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + 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): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + method="biharmonic") + +# }}} + + +# {{{ Stokeslet/Stresslet using Laplace (Tornberg) + +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. + + [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 " + "StressletWrapperTornberg") + 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=()): + 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=()): + + sym_expr = np.zeros((self.dim,), dtype=object) + + source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + common_source_kernels = [AxisSourceDerivative(k, self.kernel) for + k in range(self.dim)] + common_source_kernels.append(self.kernel) + + stresslet_weight *= 3.0/6 + stokeslet_weight *= -0.5*self.mu**(-1) + + for i in range(self.dim): + 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]) + for k in range(self.dim)] + 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), + qbx_forced_limit=qbx_forced_limit) + + if i == j: + 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) + + 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)) + 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)] + densities.append(stokeslet_weight * common_density2) + + target_kernel = 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), + qbx_forced_limit=qbx_forced_limit) + + return sym_expr + +# }}} + + +# {{{ StokesletWrapper dispatch class + +class StokesletWrapper(StokesletWrapperBase): + 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.") + 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": + 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__(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.") + 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": + 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") + # }}} @@ -518,20 +709,23 @@ class StokesOperator: .. automethod:: pressure """ - def __init__(self, ambient_dim, side): + 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. """ - if side not in [+1, -1]: raise ValueError(f"invalid evaluation side: {side}") self.ambient_dim = ambient_dim self.side = side + self.mu = mu_sym + self.nu = nu_sym - self.stresslet = StressletWrapper(dim=self.ambient_dim) - self.stokeslet = StokesletWrapper(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, + mu_sym=mu_sym, nu_sym=nu_sym, method=method) @property def dim(self): @@ -543,7 +737,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. @@ -557,13 +751,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. """ @@ -587,7 +781,8 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=None, eta=None): + 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 by :math:`A` in [HsiaoKress1985]_ Equation 2.3. @@ -595,7 +790,8 @@ def __init__(self, *, omega, alpha=None, eta=None): :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) + 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 @@ -603,61 +799,60 @@ def __init__(self, *, omega, alpha=None, eta=None): # 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 - def _farfield(self, mu, qbx_forced_limit): - length = sym.integral(self.ambient_dim, self.dim, 1) - return self.stokeslet.apply( - -self.omega / length, - mu, - qbx_forced_limit=qbx_forced_limit) - - def _operator(self, sigma, normal, 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) + # 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 if slp_qbx_forced_limit == "avg": - slp_qbx_forced_limit = +1 + slp_qbx_forced_limit = "avg" # 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)) - op_k = self.stresslet.apply(sigma, normal, mu, - qbx_forced_limit=qbx_forced_limit) - op_s = ( - self.alpha / (2.0 * np.pi) * int_sigma - - self.stokeslet.apply(meanless_sigma, mu, - qbx_forced_limit=slp_qbx_forced_limit) - ) + meanless_sigma = sym.cse(sigma - sym.mean(self.ambient_dim, + self.dim, sigma, dofdesc=dd)) - return op_k + self.eta * op_s + 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 - def prepare_rhs(self, b, *, mu): - return b + self._farfield(mu, qbx_forced_limit=+1) + return result - def operator(self, sigma, *, normal, mu): + def prepare_rhs(self, b): + 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 -0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg") + return -0.5 * self.side * sigma - self._operator( + sigma, normal, qbx_forced_limit) - 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 ( - -self._farfield(mu, qbx_forced_limit) - - self._operator(sigma, normal, mu, qbx_forced_limit) - ) + return -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 @@ -675,13 +870,15 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None): + 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. """ - super().__init__(ambient_dim=3, side=+1) + 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 @@ -689,28 +886,28 @@ def __init__(self, *, eta=None): eta = 0.75 self.eta = eta - - 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 - - op_w = self.stresslet.apply(sigma, normal, mu, - qbx_forced_limit=qbx_forced_limit) - op_v = self.stokeslet.apply(sigma, mu, - qbx_forced_limit=slp_qbx_forced_limit) - - return op_w + self.eta * op_v - - def operator(self, sigma, *, normal, mu): + self.laplace_kernel = LaplaceKernel(3) + + def _operator(self, sigma, normal, qbx_forced_limit): + # 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 - return -0.5 * self.side * sigma - self._operator(sigma, normal, mu, "avg") + return -0.5 * self.side * sigma - self._operator(sigma, + normal, qbx_forced_limit) - 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 -self._operator(sigma, normal, mu, qbx_forced_limit) + return -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/pytential/utils.py b/pytential/utils.py index 1863a30fd..1cf615c78 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(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 diff --git a/requirements.txt b/requirements.txt index 88777f180..456c4e61e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,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 diff --git a/test/test_stokes.py b/test/test_stokes.py index d1446fbbf..bb437bc3f 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -31,17 +31,17 @@ from meshmode.discretization.poly_element import \ InterpolatoryQuadratureGroupFactory 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 -from meshmode.array_context import PytestPyOpenCLArrayContextFactory import extra_int_eq_data as eid import logging logger = logging.getLogger(__name__) pytest_generate_tests = pytest_generate_tests_for_array_contexts([ - PytestPyOpenCLArrayContextFactory, + "pyopencl-deprecated", ]) @@ -68,11 +68,13 @@ def dof_array_rel_error(actx, x, xref, p=None): def run_exterior_stokes(actx_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, aspect_ratio=1.0, mu=1.0, + nu=0.4, visualize=False, + method="naive", _target_association_tolerance=0.05, _expansions_in_tree_have_extent=True): @@ -152,27 +154,41 @@ 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 = SpatialConstant("nu2") if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator sym_omega = sym.make_sym_vector("omega", ambient_dim) - op = HsiaoKressExteriorStokesOperator(omega=sym_omega) + 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() + op = HebekerExteriorStokesOperator(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_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) - sym_source_pot = op.stokeslet.apply(sym_sigma, sym_mu, 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_mu, + nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -193,20 +209,43 @@ 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} + op_context = {"mu2": mu, "normal": normal} + direct_context = {"mu2": mu} - bc = bind(places, sym_source_pot, - auto_where=("point_source", "source"))(actx, sigma=charges, mu=mu) + if sym_nu != 0.5: + bc_context["nu2"] = nu + op_context["nu2"] = nu + direct_context["nu2"] = 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) bound_op = bind(places, sym_op) # }}} + 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()} + 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) + # {{{ solve from pytential.linalg.gmres import gmres @@ -219,7 +258,6 @@ def run_exterior_stokes(actx_factory, *, progress=visualize, stall_iterations=0, hard_failure=True) - sigma = result.solution # }}} @@ -229,7 +267,8 @@ def run_exterior_stokes(actx_factory, *, velocity = bind(places, sym_velocity, auto_where=("source", "point_target"))(actx, sigma=sigma, **op_context) ref_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 = [ dof_array_rel_error(actx, u, uref) @@ -265,11 +304,18 @@ def run_exterior_stokes(actx_factory, *, return h_max, v_error -@pytest.mark.parametrize("ambient_dim", [ - 2, - pytest.param(3, marks=pytest.mark.slowtest) +@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), + pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.slowtest), + pytest.param(3, "laplace", 0.5, marks=pytest.mark.slowtest), + + (2, "biharmonic", 0.4), + pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.slowtest), + pytest.param(3, "laplace", 0.4, marks=pytest.mark.slowtest), ]) -def test_exterior_stokes(actx_factory, ambient_dim, visualize=False): +def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False): if visualize: logging.basicConfig(level=logging.INFO) @@ -281,8 +327,10 @@ def test_exterior_stokes(actx_factory, ambient_dim, visualize=False): qbx_order = 3 if ambient_dim == 2: + fmm_order = 10 resolutions = [20, 35, 50] elif ambient_dim == 3: + fmm_order = 6 resolutions = [0, 1, 2] else: raise ValueError(f"unsupported dimension: {ambient_dim}") @@ -291,9 +339,12 @@ def test_exterior_stokes(actx_factory, ambient_dim, visualize=False): h_max, errors = run_exterior_stokes(actx_factory, ambient_dim=ambient_dim, target_order=target_order, + fmm_order=fmm_order, qbx_order=qbx_order, source_ovsmp=source_ovsmp, resolution=resolution, + method=method, + nu=nu, visualize=visualize) for eoc, e in zip(eocs, errors): @@ -305,12 +356,17 @@ def test_exterior_stokes(actx_factory, ambient_dim, visualize=False): error_format="%.8e", eoc_format="%.2f")) + extra_order = 0 + if method == "biharmonic": + extra_order += 1 + 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 # }}} @@ -404,13 +460,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, - mu_sym=1, qbx_forced_limit=+1) + qbx_forced_limit=+1) def ref_result(self): return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) @@ -463,13 +519,13 @@ 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() 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() @@ -520,6 +576,14 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): if __name__ == "__main__": import sys if len(sys.argv) > 1: + import pyopencl as cl + from arraycontext import PyOpenCLArrayContext + context = cl._csc() + queue = cl.CommandQueue(context) + + def actx_factory(): + return PyOpenCLArrayContext(queue) + exec(sys.argv[1]) else: from pytest import main From e4d4e587b8f3112e570863f0343043028c493a50 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 2 Aug 2022 14:07:35 -0500 Subject: [PATCH 002/156] Point sumpy back to main --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 456c4e61e..88777f180 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,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/isuruf/sumpy.git@spatial_constant#egg=sumpy +git+https://github.com/inducer/sumpy.git#egg=sumpy git+https://github.com/inducer/pyfmmlib.git#egg=pyfmmlib From f19dccb3fd5f6b7b0a49544e6b1c961706f2a60f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 05:52:58 -0500 Subject: [PATCH 003/156] Add unit tests for pytential.symbolic.pde.system_utils --- pytential/symbolic/pde/system_utils.py | 119 +++++++++++++------------ test/test_pde_system_utils.py | 98 ++++++++++++++++++++ 2 files changed, 161 insertions(+), 56 deletions(-) create mode 100644 test/test_pde_system_utils.py diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 061c3e2e8..df50f1b5c 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -24,6 +24,7 @@ from sumpy.symbolic import make_sym_vector, SympyToPymbolicMapper import sumpy.symbolic as sym +import pymbolic from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, ExpressionKernel, KernelWrapper, TargetPointMultiplier) from pytools import (memoize_on_first_arg, @@ -74,20 +75,18 @@ def rewrite_using_base_kernel(exprs, base_kernel=_NO_ARG_SENTINEL): 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 + :class:`sumpy.kernel.AxisSourceDerivative` and 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 + and then 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 + # Convert IntGs with TargetPointMultiplier/AxisTargetDerivative to a sum of + # IntGs without TargetPointMultipliers new_int_gs = convert_target_multiplier_to_source(expr) # Convert IntGs with different kernels to expressions containing # IntGs with base_kernel or its derivatives @@ -95,40 +94,13 @@ def map_int_g(self, expr): 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 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 _get_kernel_expression(expr, kernel_arguments): + """Convert a :mod:`pymbolic` expression to :mod:`sympy` expression + after susituting kernel arguments. + + For eg: `exp(I*k*r)/r` with `{k: 1}` is converted to the sympy expression + `exp(I*r)/r` + """ from pymbolic.mapper.substitutor import substitute from sumpy.symbolic import PymbolicToSympyMapperWithSymbols @@ -139,6 +111,10 @@ def _get_kernel_expression(expr, kernel_arguments): def _monom_to_expr(monom, variables): + """Convert a monomial to an expression using given variables. + + For eg: [3, 2, 1] with variables [x, y, z] is converted to x^3 y^2 z. + """ prod = 1 for i, nrepeats in enumerate(monom): for _ in range(nrepeats): @@ -148,7 +124,7 @@ def _monom_to_expr(monom, variables): 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 + TargetMultiplier and only source dependent transformations. """ import sympy import sumpy.symbolic as sym @@ -172,6 +148,7 @@ def convert_target_multiplier_to_source(int_g): # 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]) + found = True else: return [int_g] knl = knl.inner_kernel @@ -179,44 +156,66 @@ def convert_target_multiplier_to_source(int_g): if not found: return [int_g] + int_g = int_g.copy(target_kernel=knl) + 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 = [] + # u_{d[0], d[1]}(d, y)*d[0]*y[1] + u(d, y) * d[1] + # or a single term like u(d, y) * d[1] + if isinstance(expr, sympy.Add): + args = expr.args + else: + args = [expr] - for arg in expr.args: + result = [] + for arg in args: deriv_terms = arg.atoms(sympy.Derivative) if len(deriv_terms) == 1: + # for eg: we have a term like u_{d[0], d[1]}(d, y) * d[0] * y[1] + # deriv_term is u_{d[0], d[1]} deriv_term = deriv_terms.pop() + # eg: rest_terms is d[0] * y[1] rest_terms = sympy.Poly(arg.xreplace({deriv_term: 1}), *ds, *sources) + # eg: derivatives is (d[0], 1), (d[1], 1) derivatives = deriv_term.args[1:] elif len(deriv_terms) == 0: + # for eg: we have a term like u(d, y) * d[1] + # rest_terms = d[1] rest_terms = sympy.Poly(arg.xreplace({orig_expr: 1}), *ds, *sources) derivatives = [(d, 0) for d in ds] else: 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) - 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 + # apply the derivatives + new_source_kernels = [] + for source_kernel in int_g.source_kernels: + knl = source_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)) + knl = AxisSourceDerivative(axis, knl) + new_source_kernels.append(knl) + new_int_g = int_g.copy(source_kernels=new_source_kernels) + + assert len(rest_terms.terms()) == 1 + monom, coeff = rest_terms.terms()[0] + # Now from d[0]*y[1], we separate the two terms + # d terms end up in the expression and y terms end up in the density + d_terms, y_terms = monom[:len(ds)], monom[len(ds):] + expr_multiplier = _monom_to_expr(d_terms, ds) + density_multiplier = _monom_to_expr(y_terms, sources_pymbolic) \ + * conv(coeff) + # since d/d(d) = - d/d(y), we multiply by -1 to get source derivatives + density_multiplier *= (-1)**int(sum(nrepeats for _, nrepeats in derivatives)) + new_int_gs = _multiply_int_g(new_int_g, sym.sympify(expr_multiplier), + density_multiplier) + result.extend(new_int_gs) return result def _multiply_int_g(int_g, expr_multiplier, density_multiplier): - """Multiply the exprssion in IntG with the *expr_multiplier* + """Multiply the expression in IntG with the *expr_multiplier* which is a symbolic expression and multiply the densities with *density_multiplier* which is a pymbolic expression. """ @@ -227,14 +226,22 @@ def _multiply_int_g(int_g, expr_multiplier, density_multiplier): sym_d = make_sym_vector("d", base_kernel.dim) base_kernel_expr = _get_kernel_expression(base_kernel.expression, int_g.kernel_arguments) + subst = {pymbolic.var(f"d{i}"): pymbolic.var("d")[i] for i in + range(base_kernel.dim)} conv = SympyToPymbolicMapper() + if expr_multiplier == 1: + # if there's no expr_multiplier, only multiply the densities + return [int_g.copy(densities=tuple(density*density_multiplier + for density in int_g.densities))] + for knl, density in zip(int_g.source_kernels, int_g.densities): 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_expr = pymbolic.substitute(new_expr, subst) new_knl = ExpressionKernel(knl.dim, new_expr, knl.get_base_kernel().global_scaling_const, knl.is_complex_valued) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py new file mode 100644 index 000000000..6f25060ea --- /dev/null +++ b/test/test_pde_system_utils.py @@ -0,0 +1,98 @@ +__copyright__ = "Copyright (C) 2022 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 pytential.symbolic.pde.system_utils import convert_target_multiplier_to_source +from pytential.symbolic.primitives import IntG +from pytential.symbolic.primitives import NodeCoordinateComponent + +from sumpy.kernel import (LaplaceKernel, HelmholtzKernel, AxisTargetDerivative, + TargetPointMultiplier, AxisSourceDerivative, ExpressionKernel) + +from pymbolic.primitives import make_sym_vector +import pymbolic.primitives as prim + + +def test_convert_target_deriv(): + knl = LaplaceKernel(2) + int_g = IntG(AxisTargetDerivative(0, knl), [AxisSourceDerivative(1, knl), knl], + [1, 2], qbx_forced_limit=1) + expected_int_g = IntG(knl, + [AxisSourceDerivative(0, AxisSourceDerivative(1, knl)), + AxisSourceDerivative(0, knl)], [-1, -2], qbx_forced_limit=1) + + assert sum(convert_target_multiplier_to_source(int_g)) == expected_int_g + + +def test_convert_target_point_multiplier(): + xs = [NodeCoordinateComponent(i) for i in range(3)] + + knl = LaplaceKernel(3) + int_g = IntG(TargetPointMultiplier(0, knl), [AxisSourceDerivative(1, knl), knl], + [1, 2], qbx_forced_limit=1) + + d = make_sym_vector("d", 3) + r2 = d[2]**2 + d[1]**2 + d[0]**2 + eknl1 = ExpressionKernel(3, d[1]*d[0]*r2**prim.Quotient(-3, 2), + knl.global_scaling_const, False) + eknl2 = ExpressionKernel(3, d[0]*r2**prim.Quotient(-1, 2), + knl.global_scaling_const, False) + expected_int_g = IntG(eknl1, [eknl1], [1], qbx_forced_limit=1) + \ + IntG(eknl2, [eknl2], [2], qbx_forced_limit=1) + \ + IntG(knl, [AxisSourceDerivative(1, knl), knl], + [xs[0], 2*xs[0]], qbx_forced_limit=1) + + assert expected_int_g == sum(convert_target_multiplier_to_source(int_g)) + + +def test_product_rule(): + xs = [NodeCoordinateComponent(i) for i in range(3)] + + knl = LaplaceKernel(3) + int_g = IntG(AxisTargetDerivative(0, TargetPointMultiplier(0, knl)), [knl], [1], + qbx_forced_limit=1) + + d = make_sym_vector("d", 3) + r2 = d[2]**2 + d[1]**2 + d[0]**2 + eknl = ExpressionKernel(3, d[0]**2*r2**prim.Quotient(-3, 2), + knl.global_scaling_const, False) + expected_int_g = IntG(eknl, [eknl], [-1], qbx_forced_limit=1) + \ + IntG(knl, [AxisSourceDerivative(0, knl)], [xs[0]*(-1)], qbx_forced_limit=1) + + assert expected_int_g == sum(convert_target_multiplier_to_source(int_g)) + + +def test_convert_helmholtz(): + xs = [NodeCoordinateComponent(i) for i in range(3)] + + knl = HelmholtzKernel(3) + int_g = IntG(TargetPointMultiplier(0, knl), [knl], [1], + qbx_forced_limit=1, k=1) + + d = make_sym_vector("d", 3) + r2 = (d[2]**2 + d[1]**2 + d[0]**2) + exp = prim.Variable("exp") + eknl = ExpressionKernel(3, exp(1j*r2**prim.Quotient(1, 2))*d[0] + * r2**prim.Quotient(-1, 2), + knl.global_scaling_const, knl.is_complex_valued) + expected_int_g = IntG(eknl, [eknl], [1], qbx_forced_limit=1, + kernel_arguments={"k": 1}) + \ + IntG(knl, [knl], [xs[0]], qbx_forced_limit=1, k=1) + + assert expected_int_g == sum(convert_target_multiplier_to_source(int_g)) From 25a14cb37ac596fd7707b2ca0f8ae367fd745ee3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 06:34:36 -0500 Subject: [PATCH 004/156] Throw RuntimeError --- 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 df50f1b5c..74f4cc078 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -60,11 +60,11 @@ def rewrite_using_base_kernel(exprs, base_kernel=_NO_ARG_SENTINEL): """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 + 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. + The routine will fail with a ``RuntimeError`` if this process cannot be completed. """ if base_kernel is None: return list(exprs) @@ -361,7 +361,7 @@ def _get_base_kernel_matrix(base_kernel, order=None, retries=3, order = pde.order if order > pde.order: - raise NotImplementedError(f"order ({order}) cannot be greater than the order" + raise RuntimeError(f"order ({order}) cannot be greater than the order" f"of the PDE ({pde.order}) yet.") mis = sorted(gnitstam(order, dim), key=sum) From f492f6e65c9c2a39a8ab4d63e479a91f7cfc961b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 06:35:04 -0500 Subject: [PATCH 005/156] Double back-ticks around IntG --- pytential/symbolic/pde/system_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 74f4cc078..dc09112ac 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -73,10 +73,10 @@ def rewrite_using_base_kernel(exprs, base_kernel=_NO_ARG_SENTINEL): class RewriteUsingBaseKernelMapper(IdentityMapper): - """Rewrites IntGs using the base kernel. First this method replaces - IntGs with :class:`sumpy.kernel.AxisTargetDerivative` to IntGs + """Rewrites ``IntG``s using the base kernel. First this method replaces + ``IntG``s with :class:`sumpy.kernel.AxisTargetDerivative` to ``IntG``s :class:`sumpy.kernel.AxisSourceDerivative` and - IntGs with :class:`sumpy.kernel.TargetPointMultiplier` to IntGs + ``IntG``s with :class:`sumpy.kernel.TargetPointMultiplier` to ``IntG``s without them using :class:`sumpy.kernel.ExpressionKernel` and then converts them to the base kernel by finding a relationship between the derivatives. @@ -123,7 +123,7 @@ def _monom_to_expr(monom, variables): def convert_target_multiplier_to_source(int_g): - """Convert an IntG with TargetMultiplier to a sum of IntGs without + """Convert an ``IntG`` with TargetMultiplier to a sum of ``IntG``s without TargetMultiplier and only source dependent transformations. """ import sympy @@ -215,7 +215,7 @@ def convert_target_multiplier_to_source(int_g): def _multiply_int_g(int_g, expr_multiplier, density_multiplier): - """Multiply the expression in IntG with the *expr_multiplier* + """Multiply the expression in ``IntG`` with the *expr_multiplier* which is a symbolic expression and multiply the densities with *density_multiplier* which is a pymbolic expression. """ From ceea82cead3cce9beaf3fd263e4e806e36836f1e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 13:44:09 -0500 Subject: [PATCH 006/156] typing --- pytential/symbolic/pde/system_utils.py | 54 +++++++++++++++++++------- pytential/symbolic/typing.py | 31 +++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 pytential/symbolic/typing.py diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index dc09112ac..8ecd96f02 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -36,6 +36,10 @@ from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand import pytential +from pytential.symbolic import IntG + +from typing import List, Mapping, Text, Any, Union, Tuple +from pytential.symbolic.typing import ExpressionT import logging logger = logging.getLogger(__name__) @@ -56,7 +60,8 @@ _NO_ARG_SENTINEL = object() -def rewrite_using_base_kernel(exprs, base_kernel=_NO_ARG_SENTINEL): +def rewrite_using_base_kernel(exprs: List[ExpressionT], + base_kernel=_NO_ARG_SENTINEL) -> List[ExpressionT]: """Rewrites an expression with :class:`~pytential.symbolic.primitives.IntG` objects using *base_kernel*. @@ -64,7 +69,8 @@ def rewrite_using_base_kernel(exprs, base_kernel=_NO_ARG_SENTINEL): is encountered, this will (forcibly) rewrite the Laplace kernel in terms of derivatives of the Biharmonic kernel. - The routine will fail with a ``RuntimeError`` if this process cannot be completed. + The routine will fail with a ``RuntimeError`` if this process cannot be + completed. """ if base_kernel is None: return list(exprs) @@ -94,7 +100,8 @@ def map_int_g(self, expr): self.base_kernel) for new_int_g in new_int_gs) -def _get_kernel_expression(expr, kernel_arguments): +def _get_kernel_expression(expr: ExpressionT, + kernel_arguments: Mapping[Text, Any]) -> sym.Basic: """Convert a :mod:`pymbolic` expression to :mod:`sympy` expression after susituting kernel arguments. @@ -110,7 +117,9 @@ def _get_kernel_expression(expr, kernel_arguments): return res -def _monom_to_expr(monom, variables): +def _monom_to_expr(monom: List[int], + variables: List[Union[sym.Basic, ExpressionT]]) \ + -> Union[sym.Basic, ExpressionT]: """Convert a monomial to an expression using given variables. For eg: [3, 2, 1] with variables [x, y, z] is converted to x^3 y^2 z. @@ -122,7 +131,7 @@ def _monom_to_expr(monom, variables): return prod -def convert_target_multiplier_to_source(int_g): +def convert_target_multiplier_to_source(int_g: IntG) -> List[IntG]: """Convert an ``IntG`` with TargetMultiplier to a sum of ``IntG``s without TargetMultiplier and only source dependent transformations. """ @@ -214,7 +223,8 @@ def convert_target_multiplier_to_source(int_g): return result -def _multiply_int_g(int_g, expr_multiplier, density_multiplier): +def _multiply_int_g(int_g: IntG, expr_multiplier: sym.Basic, + density_multiplier: ExpressionT) -> List[IntG]: """Multiply the expression in ``IntG`` with the *expr_multiplier* which is a symbolic expression and multiply the densities with *density_multiplier* which is a pymbolic expression. @@ -251,7 +261,8 @@ def _multiply_int_g(int_g, expr_multiplier, density_multiplier): return result -def convert_int_g_to_base(int_g, base_kernel): +def convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ + -> ExpressionT: result = 0 for knl, density in zip(int_g.source_kernels, int_g.densities): result += _convert_int_g_to_base( @@ -260,7 +271,8 @@ def convert_int_g_to_base(int_g, base_kernel): return result -def _convert_int_g_to_base(int_g, base_kernel): +def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ + -> ExpressionT: target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) dim = target_kernel.dim @@ -304,8 +316,12 @@ def _convert_int_g_to_base(int_g, base_kernel): return result -def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, - kernel_arguments=None): +def get_deriv_relation(kernels: List[ExpressionKernel], + base_kernel: ExpressionKernel, + kernel_arguments: Mapping[Text, Any], + tol: float = 1e-10, + order: Union[None, int] = None) \ + -> List[Tuple[ExpressionT, ExpressionT]]: res = [] for knl in kernels: res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order, @@ -314,10 +330,15 @@ def get_deriv_relation(kernels, base_kernel, tol=1e-10, order=None, @memoize_on_first_arg -def get_deriv_relation_kernel(kernel, base_kernel, tol=1e-10, order=None, - hashable_kernel_arguments=None): +def get_deriv_relation_kernel(kernel: ExpressionKernel, + base_kernel: ExpressionKernel, + hashable_kernel_arguments: List[Tuple[Text, Any]], + tol: float = 1e-10, + order: Union[None, int] = None) \ + -> Tuple[ExpressionT, ExpressionT]: kernel_arguments = dict(hashable_kernel_arguments) - (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order) + (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, + kernel_arguments=kernel_arguments) dim = base_kernel.dim sym_vec = make_sym_vector("d", dim) sympy_conv = SympyToPymbolicMapper() @@ -352,8 +373,11 @@ 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: ExpressionKernel, + kernel_arguments: Mapping[Text, Any], + order: Union[None, int] = None, retries: int = 3) \ + -> Tuple[Tuple[sym.Matrix, sym.Matrix, List[Tuple[int, int]]], + np.ndarray, List[Tuple[int]]]: dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() diff --git a/pytential/symbolic/typing.py b/pytential/symbolic/typing.py new file mode 100644 index 000000000..7a5fac687 --- /dev/null +++ b/pytential/symbolic/typing.py @@ -0,0 +1,31 @@ +__copyright__ = "Copyright (C) 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 typing import Union +import numpy as np +from pymbolic.primitives import Expression + +IntegralT = Union[int, np.int8, np.int16, np.int32, np.int64, np.uint8, + np.uint16, np.uint32, np.uint64] +FloatT = Union[float, complex, np.float32, np.float64, np.complex64, + np.complex128] + +ExpressionT = Union[IntegralT, FloatT, Expression] From 8cc7b24d3817f2d96b9737a64723e1c31eddce50 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 13:50:15 -0500 Subject: [PATCH 007/156] Add an example to convert_target_transformation_to_source --- pytential/symbolic/pde/system_utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 8ecd96f02..591937725 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -93,7 +93,7 @@ def __init__(self, base_kernel): def map_int_g(self, expr): # Convert IntGs with TargetPointMultiplier/AxisTargetDerivative to a sum of # IntGs without TargetPointMultipliers - new_int_gs = convert_target_multiplier_to_source(expr) + new_int_gs = convert_target_transformation_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, @@ -131,9 +131,15 @@ def _monom_to_expr(monom: List[int], return prod -def convert_target_multiplier_to_source(int_g: IntG) -> List[IntG]: - """Convert an ``IntG`` with TargetMultiplier to a sum of ``IntG``s without - TargetMultiplier and only source dependent transformations. +def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: + """Convert an ``IntG`` with AxisTargetDerivative/TargetMultiplier to a list + of ``IntG``s without them and only source dependent transformations. + The sum of the list returned is a tranformation of the input ``IntG``. + + eg:: + + IntG(d/dx r, sigma) -> [IntG(d/dy r, -sigma)] + IntG(x*r, sigma) -> [IntG(r, sigma*y), IntG(r*(x -y), sigma)] """ import sympy import sumpy.symbolic as sym From 2652ace38803e2f1b18d997fc0ac43e832319c94 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 13:50:55 -0500 Subject: [PATCH 008/156] use simpler expression --- pytential/symbolic/pde/system_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 591937725..90eb5a741 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -213,8 +213,7 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: new_source_kernels.append(knl) new_int_g = int_g.copy(source_kernels=new_source_kernels) - assert len(rest_terms.terms()) == 1 - monom, coeff = rest_terms.terms()[0] + (monom, coeff,) = rest_terms.terms()[0] # Now from d[0]*y[1], we separate the two terms # d terms end up in the expression and y terms end up in the density d_terms, y_terms = monom[:len(ds)], monom[len(ds):] From 81e449aa3dafda1ed64abc40c82d7d5b67b1c922 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 16:17:10 -0500 Subject: [PATCH 009/156] Add abstractmethod --- pytential/symbolic/stokes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index c325dd3a0..08c62dd94 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -28,7 +28,7 @@ ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant -from abc import ABC +from abc import ABC, abstractmethod __doc__ = """ .. autoclass:: StokesletWrapper @@ -744,6 +744,7 @@ def prepare_rhs(self, b): """ return b + @abstractmethod def operator(self, sigma): """ :returns: the integral operator that should be solved to obtain the @@ -751,12 +752,14 @@ def operator(self, sigma): """ raise NotImplementedError + @abstractmethod def velocity(self, sigma, *, normal, qbx_forced_limit=None): """ :returns: a representation of the velocity field in the Stokes flow. """ raise NotImplementedError + @abstractmethod def pressure(self, sigma, *, normal, qbx_forced_limit=None): """ :returns: a representation of the pressure in the Stokes flow. From 3cf612c7e862bb03b5759c94359caa1c8b8b2023 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 16:26:48 -0500 Subject: [PATCH 010/156] abstractmethod --- pytential/symbolic/stokes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 08c62dd94..43df73dd9 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -122,6 +122,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): """ return self.apply(density_vec_sym, qbx_forced_limit, [deriv_dir]) + @abstractmethod def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. @@ -179,6 +180,7 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym + @abstractmethod def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """Symbolic expressions for integrating Stresslet kernel. @@ -236,6 +238,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, return self.apply(density_vec_sym, dir_vec_sym, qbx_forced_limit, [deriv_dir]) + @abstractmethod 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. @@ -550,6 +553,9 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) + def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + raise NotImplementedError + class StressletWrapperTornberg(StressletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which From 56755c851e8fcfc1c44b32e4e08e3eefb9871831 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 16:44:16 -0500 Subject: [PATCH 011/156] simplify loops --- pytential/symbolic/stokes.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 43df73dd9..0e2a21adc 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -303,17 +303,14 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, # 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, poisson_ratio=poisson_ratio) - # 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)] + for j in range(i, dim): + self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, + jcomp=j, poisson_ratio=poisson_ratio) + self.kernel_dict[(j, i)] = self.kernel_dict[(i, j)] def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): From 4683196f0b25a99906206f074165eb79b7e6ecaa Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 16:46:29 -0500 Subject: [PATCH 012/156] remove unnecessary code --- pytential/symbolic/stokes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 0e2a21adc..1b099534d 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -822,10 +822,6 @@ def _farfield(self, qbx_forced_limit): return result 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" - # 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` From 0e1a9b396850db9bdfacf074571e6cfbb28153f3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 16:56:40 -0500 Subject: [PATCH 013/156] list -> tuple --- 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 1b099534d..e54865d5c 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -120,7 +120,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`. """ - return self.apply(density_vec_sym, qbx_forced_limit, [deriv_dir]) + return self.apply(density_vec_sym, qbx_forced_limit, (deriv_dir,)) @abstractmethod def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): @@ -236,7 +236,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, to :class:`~pytential.symbolic.primitives.IntG`. """ return self.apply(density_vec_sym, dir_vec_sym, qbx_forced_limit, - [deriv_dir]) + (deriv_dir,)) @abstractmethod def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, From 52b6beb7e6a64bf36b0d5904f77972c4457499df Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 17:03:45 -0500 Subject: [PATCH 014/156] Pass base_kernel in WrapperNaiveOrBiharmonic --- pytential/symbolic/stokes.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index e54865d5c..5264a2620 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -285,18 +285,12 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): class _StokesletWrapperNaiveOrBiharmonic(StokesletWrapperBase): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, - method="biharmonic"): + base_kernel=None): 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.base_kernel = base_kernel self.kernel_dict = {} # The two cases of nu=0.5 and nu!=0.5 differ significantly and @@ -342,8 +336,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) + stresslet_obj = _StressletWrapperNaiveOrBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) # For stokeslet, there's no direction vector involved # passing a list of ones instead to remove its usage. @@ -364,13 +358,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): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - method="naive") + base_kernel=None) class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - method="biharmonic") + base_kernel=BiharmonicKernel(dim)) # }}} @@ -380,18 +374,12 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): class _StressletWrapperNaiveOrBiharmonic(StressletWrapperBase): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, - method="biharmonic"): + base_kernel=None): 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.base_kernel = base_kernel self.kernel_dict = {} @@ -466,8 +454,8 @@ 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) + stokeslet_obj = _StokesletWrapperNaiveOrBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) sym_expr = 0 if stresslet_weight != 0: @@ -511,13 +499,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): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - method="naive") + base_kernel=None) class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic): def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - method="biharmonic") + base_kernel=BiharmonicKernel(dim)) # }}} From 1626131b232226ffa23ee1bce96d71d8bce363b4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 17:09:58 -0500 Subject: [PATCH 015/156] No abstractmethod for apply_stress for now --- pytential/symbolic/stokes.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 5264a2620..09ea67c19 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -122,7 +122,6 @@ def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): """ return self.apply(density_vec_sym, qbx_forced_limit, (deriv_dir,)) - @abstractmethod def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. @@ -238,7 +237,6 @@ def apply_derivative(self, deriv_dir, density_vec_sym, dir_vec_sym, return self.apply(density_vec_sym, dir_vec_sym, qbx_forced_limit, (deriv_dir,)) - @abstractmethod 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. @@ -538,9 +536,6 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) - def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): - raise NotImplementedError - class StressletWrapperTornberg(StressletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which From 5f570f521463325fb742a8e180be49a756bf2f10 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 18:17:43 -0500 Subject: [PATCH 016/156] correct some typos to fix tests --- pytential/symbolic/pde/system_utils.py | 11 ++++++----- pytential/symbolic/stokes.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 90eb5a741..76a27cbeb 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -32,11 +32,10 @@ as gnitstam) from pytential.symbolic.primitives import (NodeCoordinateComponent, - hashable_kernel_args) + hashable_kernel_args, IntG) from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_solve_with_expand import pytential -from pytential.symbolic import IntG from typing import List, Mapping, Text, Any, Union, Tuple from pytential.symbolic.typing import ExpressionT @@ -329,8 +328,9 @@ def get_deriv_relation(kernels: List[ExpressionKernel], -> List[Tuple[ExpressionT, ExpressionT]]: res = [] for knl in kernels: - res.append(get_deriv_relation_kernel(knl, base_kernel, tol, order, - hashable_kernel_arguments=hashable_kernel_args(kernel_arguments))) + res.append(get_deriv_relation_kernel(knl, base_kernel, + hashable_kernel_arguments=hashable_kernel_args(kernel_arguments), + tol=tol, order=order)) return res @@ -446,6 +446,7 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, raise RuntimeError("Failed to find a base kernel") return _get_base_kernel_matrix( base_kernel=base_kernel, + kernel_arguments=kernel_arguments, order=order, retries=retries-1, ) @@ -499,4 +500,4 @@ def filter_kernel_arguments(knls, kernel_arguments): expression_knl2 = ExpressionKernel(3, conv(1/sym_r + sym_d[0]*sym_d[0]/sym_r**3), 1, False) kernels = [expression_knl, expression_knl2] - get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4) + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, kernel_arguments={}) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 09ea67c19..0ebbc03ab 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -77,6 +77,7 @@ def __init__(self, dim, mu_sym, nu_sym): self.mu = mu_sym self.nu = nu_sym + @abstractmethod def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """Symbolic expressions for integrating Stokeslet kernel. From b0dc5637f675102b4c9c6206d8464ae5ff3f4ee3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 18:19:17 -0500 Subject: [PATCH 017/156] Fix refactor --- test/test_pde_system_utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 6f25060ea..0022a14ad 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -18,7 +18,8 @@ THE SOFTWARE. """ -from pytential.symbolic.pde.system_utils import convert_target_multiplier_to_source +from pytential.symbolic.pde.system_utils import ( + convert_target_transformation_to_source) from pytential.symbolic.primitives import IntG from pytential.symbolic.primitives import NodeCoordinateComponent @@ -37,7 +38,7 @@ def test_convert_target_deriv(): [AxisSourceDerivative(0, AxisSourceDerivative(1, knl)), AxisSourceDerivative(0, knl)], [-1, -2], qbx_forced_limit=1) - assert sum(convert_target_multiplier_to_source(int_g)) == expected_int_g + assert sum(convert_target_transformation_to_source(int_g)) == expected_int_g def test_convert_target_point_multiplier(): @@ -58,7 +59,7 @@ def test_convert_target_point_multiplier(): IntG(knl, [AxisSourceDerivative(1, knl), knl], [xs[0], 2*xs[0]], qbx_forced_limit=1) - assert expected_int_g == sum(convert_target_multiplier_to_source(int_g)) + assert expected_int_g == sum(convert_target_transformation_to_source(int_g)) def test_product_rule(): @@ -75,7 +76,7 @@ def test_product_rule(): expected_int_g = IntG(eknl, [eknl], [-1], qbx_forced_limit=1) + \ IntG(knl, [AxisSourceDerivative(0, knl)], [xs[0]*(-1)], qbx_forced_limit=1) - assert expected_int_g == sum(convert_target_multiplier_to_source(int_g)) + assert expected_int_g == sum(convert_target_transformation_to_source(int_g)) def test_convert_helmholtz(): @@ -95,4 +96,4 @@ def test_convert_helmholtz(): kernel_arguments={"k": 1}) + \ IntG(knl, [knl], [xs[0]], qbx_forced_limit=1, k=1) - assert expected_int_g == sum(convert_target_multiplier_to_source(int_g)) + assert expected_int_g == sum(convert_target_transformation_to_source(int_g)) From 111b28bed05f3f0380fa6df41ef56283f25aa0dd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 18:33:07 -0500 Subject: [PATCH 018/156] Turn StokesletWrapper into a function --- pytential/symbolic/stokes.py | 86 ++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 0ebbc03ab..5199ae4f8 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -630,52 +630,50 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, # {{{ StokesletWrapper dispatch class -class StokesletWrapper(StokesletWrapperBase): - 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.") - 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": - 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) +def StokesletWrapper(dim, 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.") + 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": + if nu_sym == 0.5: + 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__(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.") - 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": - 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) + 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") + + +def StressletWrapper(dim, 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.") + 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": + if nu_sym == 0.5: + 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") + 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") # }}} From 95466cc214884cd36c52e8d5e043cef3d004677b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 19:36:48 -0500 Subject: [PATCH 019/156] more testing --- pytential/symbolic/pde/system_utils.py | 23 +++++++----- test/test_pde_system_utils.py | 52 ++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 76a27cbeb..65bb397cd 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -297,7 +297,7 @@ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ 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] + return int_g if source_kernel != source_kernel.get_base_kernel(): # We assume that any source transformation is a derivative @@ -337,13 +337,13 @@ def get_deriv_relation(kernels: List[ExpressionKernel], @memoize_on_first_arg def get_deriv_relation_kernel(kernel: ExpressionKernel, base_kernel: ExpressionKernel, - hashable_kernel_arguments: List[Tuple[Text, Any]], + hashable_kernel_arguments: Tuple[Tuple[Text, Any]], tol: float = 1e-10, order: Union[None, int] = None) \ -> Tuple[ExpressionT, ExpressionT]: kernel_arguments = dict(hashable_kernel_arguments) (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, - kernel_arguments=kernel_arguments) + hashable_kernel_arguments=hashable_kernel_arguments) dim = base_kernel.dim sym_vec = make_sym_vector("d", dim) sympy_conv = SympyToPymbolicMapper() @@ -379,7 +379,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, @memoize_on_first_arg def _get_base_kernel_matrix(base_kernel: ExpressionKernel, - kernel_arguments: Mapping[Text, Any], + hashable_kernel_arguments: Tuple[Tuple[Text, Any]], order: Union[None, int] = None, retries: int = 3) \ -> Tuple[Tuple[sym.Matrix, sym.Matrix, List[Tuple[int, int]]], np.ndarray, List[Tuple[int]]]: @@ -410,7 +410,8 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, rand[i, j] = sym.sympify(rand[i, j])/10**15 sym_vec = make_sym_vector("d", dim) - base_expr = _get_kernel_expression(base_kernel.expression, kernel_arguments) + base_expr = _get_kernel_expression(base_kernel.expression, + dict(hashable_kernel_arguments)) mat = [] for rand_vec_idx in range(rand.shape[1]): @@ -446,7 +447,7 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, raise RuntimeError("Failed to find a base kernel") return _get_base_kernel_matrix( base_kernel=base_kernel, - kernel_arguments=kernel_arguments, + hashable_kernel_arguments=hashable_kernel_arguments, order=order, retries=retries-1, ) @@ -487,12 +488,14 @@ def filter_kernel_arguments(knls, kernel_arguments): logging.basicConfig(level=logging.DEBUG) from sumpy.kernel import (StokesletKernel, BiharmonicKernel, # noqa:F401 StressletKernel, ElasticityKernel, LaplaceKernel) - base_kernel = BiharmonicKernel(3) + base_kernel = BiharmonicKernel(2) #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)] + kernels = [StokesletKernel(2, 0, 1), StokesletKernel(2, 0, 0)] + kernels += [StressletKernel(2, 0, 0, 0), StressletKernel(2, 0, 0, 1), + StressletKernel(2, 0, 0, 1), StressletKernel(2, 0, 1, 1)] + get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, kernel_arguments={}) + base_kernel = BiharmonicKernel(3) sym_d = make_sym_vector("d", base_kernel.dim) sym_r = sym.sqrt(sum(a**2 for a in sym_d)) conv = SympyToPymbolicMapper() diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 0022a14ad..012d70f5b 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -19,12 +19,16 @@ """ from pytential.symbolic.pde.system_utils import ( - convert_target_transformation_to_source) + convert_target_transformation_to_source, convert_int_g_to_base) from pytential.symbolic.primitives import IntG from pytential.symbolic.primitives import NodeCoordinateComponent +import pytential +import numpy as np -from sumpy.kernel import (LaplaceKernel, HelmholtzKernel, AxisTargetDerivative, - TargetPointMultiplier, AxisSourceDerivative, ExpressionKernel) +from sumpy.kernel import ( + LaplaceKernel, HelmholtzKernel, ExpressionKernel, BiharmonicKernel, + StokesletKernel, + AxisTargetDerivative, TargetPointMultiplier, AxisSourceDerivative) from pymbolic.primitives import make_sym_vector import pymbolic.primitives as prim @@ -97,3 +101,45 @@ def test_convert_helmholtz(): IntG(knl, [knl], [xs[0]], qbx_forced_limit=1, k=1) assert expected_int_g == sum(convert_target_transformation_to_source(int_g)) + + +def test_convert_int_g_base(): + knl = LaplaceKernel(3) + int_g = IntG(knl, [knl], [1], qbx_forced_limit=1) + + base_knl = BiharmonicKernel(3) + expected_int_g = sum( + IntG(base_knl, [AxisSourceDerivative(d, AxisSourceDerivative(d, base_knl))], + [-1], qbx_forced_limit=1) for d in range(3)) + + assert expected_int_g == convert_int_g_to_base(int_g, base_kernel=base_knl) + + +def test_convert_int_g_base_with_const(): + knl = StokesletKernel(2, 0, 0) + int_g = IntG(knl, [knl], [1], qbx_forced_limit=1, mu=2) + + base_knl = BiharmonicKernel(2) + dim = 2 + dd = pytential.sym.DOFDescriptor(None, + discr_stage=pytential.sym.QBX_SOURCE_STAGE1) + + expected_int_g = (-0.1875)*prim.Power(np.pi, -1) * \ + pytential.sym.integral(dim, dim-1, 1, dofdesc=dd) + \ + IntG(base_knl, + [AxisSourceDerivative(1, AxisSourceDerivative(1, base_knl))], [0.5], + qbx_forced_limit=1) + assert convert_int_g_to_base(int_g, base_kernel=base_knl) == expected_int_g + + +def test_convert_int_g_base_with_const_and_deriv(): + knl = StokesletKernel(2, 0, 0) + int_g = IntG(knl, [AxisSourceDerivative(0, knl)], [1], qbx_forced_limit=1, mu=2) + + base_knl = BiharmonicKernel(2) + + expected_int_g = IntG(base_knl, + [AxisSourceDerivative(1, AxisSourceDerivative(1, + AxisSourceDerivative(0, base_knl)))], [0.5], + qbx_forced_limit=1) + assert convert_int_g_to_base(int_g, base_kernel=base_knl) == expected_int_g From ae4413a1c7b37b94d190ce723b7505ed682b008f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Sep 2022 22:23:56 -0500 Subject: [PATCH 020/156] Fix tests for sympy --- test/test_pde_system_utils.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 012d70f5b..037f375a1 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -29,6 +29,7 @@ LaplaceKernel, HelmholtzKernel, ExpressionKernel, BiharmonicKernel, StokesletKernel, AxisTargetDerivative, TargetPointMultiplier, AxisSourceDerivative) +from sumpy.symbolic import USE_SYMENGINE from pymbolic.primitives import make_sym_vector import pymbolic.primitives as prim @@ -53,9 +54,15 @@ def test_convert_target_point_multiplier(): [1, 2], qbx_forced_limit=1) d = make_sym_vector("d", 3) - r2 = d[2]**2 + d[1]**2 + d[0]**2 - eknl1 = ExpressionKernel(3, d[1]*d[0]*r2**prim.Quotient(-3, 2), - knl.global_scaling_const, False) + + if USE_SYMENGINE: + r2 = d[2]**2 + d[1]**2 + d[0]**2 + eknl1 = ExpressionKernel(3, d[1]*d[0]*r2**prim.Quotient(-3, 2), + knl.global_scaling_const, False) + else: + r2 = d[0]**2 + d[1]**2 + d[2]**2 + eknl1 = ExpressionKernel(3, d[0]*d[1]*r2**prim.Quotient(-3, 2), + knl.global_scaling_const, False) eknl2 = ExpressionKernel(3, d[0]*r2**prim.Quotient(-1, 2), knl.global_scaling_const, False) expected_int_g = IntG(eknl1, [eknl1], [1], qbx_forced_limit=1) + \ @@ -74,7 +81,10 @@ def test_product_rule(): qbx_forced_limit=1) d = make_sym_vector("d", 3) - r2 = d[2]**2 + d[1]**2 + d[0]**2 + if USE_SYMENGINE: + r2 = d[2]**2 + d[1]**2 + d[0]**2 + else: + r2 = d[0]**2 + d[1]**2 + d[2]**2 eknl = ExpressionKernel(3, d[0]**2*r2**prim.Quotient(-3, 2), knl.global_scaling_const, False) expected_int_g = IntG(eknl, [eknl], [-1], qbx_forced_limit=1) + \ @@ -91,11 +101,19 @@ def test_convert_helmholtz(): qbx_forced_limit=1, k=1) d = make_sym_vector("d", 3) - r2 = (d[2]**2 + d[1]**2 + d[0]**2) exp = prim.Variable("exp") - eknl = ExpressionKernel(3, exp(1j*r2**prim.Quotient(1, 2))*d[0] - * r2**prim.Quotient(-1, 2), - knl.global_scaling_const, knl.is_complex_valued) + + if USE_SYMENGINE: + r2 = d[2]**2 + d[1]**2 + d[0]**2 + eknl = ExpressionKernel(3, exp(1j*r2**prim.Quotient(1, 2))*d[0] + * r2**prim.Quotient(-1, 2), + knl.global_scaling_const, knl.is_complex_valued) + else: + r2 = d[0]**2 + d[1]**2 + d[2]**2 + eknl = ExpressionKernel(3, d[0]*r2**prim.Quotient(-1, 2) + * exp(1j*r2**prim.Quotient(1, 2)), + knl.global_scaling_const, knl.is_complex_valued) + expected_int_g = IntG(eknl, [eknl], [1], qbx_forced_limit=1, kernel_arguments={"k": 1}) + \ IntG(knl, [knl], [xs[0]], qbx_forced_limit=1, k=1) From b6d4b142eadaa3c8131ae4850df99c70b57abc00 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 7 Sep 2022 16:36:07 -0500 Subject: [PATCH 021/156] rewrite_using_base_kernel for pressure as well --- pytential/symbolic/pde/system_utils.py | 19 +++++++++---- pytential/symbolic/stokes.py | 39 ++++++++++++++++---------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 65bb397cd..4630b862d 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -61,13 +61,17 @@ def rewrite_using_base_kernel(exprs: List[ExpressionT], base_kernel=_NO_ARG_SENTINEL) -> List[ExpressionT]: - """Rewrites an expression with :class:`~pytential.symbolic.primitives.IntG` + """ + Rewrites a list of expressions 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. + If *base_kernel* is *None*, the expression list is returned as is. + If *base_kernel* is not given, the method will try to find a base kernel. + The routine will fail with a ``RuntimeError`` if this process cannot be completed. """ @@ -93,10 +97,15 @@ def map_int_g(self, expr): # Convert IntGs with TargetPointMultiplier/AxisTargetDerivative to a sum of # IntGs without TargetPointMultipliers new_int_gs = convert_target_transformation_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) + # if base_kernel was not given, use the first we find + if self.base_kernel == _NO_ARG_SENTINEL: + self.base_kernel = int_g.target_kernel.get_base_kernel() + return int_g + else: + # 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 _get_kernel_expression(expr: ExpressionT, diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 5199ae4f8..9174da9f1 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -93,20 +93,20 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """ raise NotImplementedError - def apply_pressure(self, density_vec_sym, qbx_forced_limit): + def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """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. + lknl = LaplaceKernel(dim=self.dim) - 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))) - + deriv_dirs = tuple(extra_deriv_dirs) + (i,) + knl = lknl + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + sym_expr += sym.int_g_vec(knl, density_vec_sym[i], + qbx_forced_limit=qbx_forced_limit) return sym_expr def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): @@ -198,26 +198,28 @@ 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): + def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=()): """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) + lknl = LaplaceKernel(dim=self.dim) factor = (2. * self.mu) 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.int_g_vec(kernel, + deriv_dirs = tuple(extra_deriv_dirs) + (i, j) + knl = lknl + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + sym_expr += factor * sym.int_g_vec(knl, density_vec_sym[i] * dir_vec_sym[j], - qbx_forced_limit=qbx_forced_limit))) + qbx_forced_limit=qbx_forced_limit) return sym_expr @@ -332,6 +334,13 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): return np.array(rewrite_using_base_kernel(sym_expr, base_kernel=self.base_kernel)) + def apply_pressure(self, density_vec_sym, qbx_forced_limit, + extra_deriv_dirs=()): + sym_expr = super().apply_pressure(density_vec_sym, qbx_forced_limit, + extra_deriv_dirs=extra_deriv_dirs) + res, = rewrite_using_base_kernel([sym_expr], base_kernel=self.base_kernel) + return res + def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) From da051290d5b274c840b85cceb3e7b81f8e5a5459 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 7 Sep 2022 16:36:49 -0500 Subject: [PATCH 022/156] test PDE --- test/test_stokes.py | 115 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 3 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index bb437bc3f..17b7d383a 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -27,6 +27,8 @@ from arraycontext import flatten from pytential import GeometryCollection, bind, sym +from pytential.symbolic.stokes import (StokesletWrapper, StressletWrapper, + StressletWrapperBase) from meshmode.discretization import Discretization from meshmode.discretization.poly_element import \ InterpolatoryQuadratureGroupFactory @@ -186,7 +188,6 @@ def run_exterior_stokes(actx_factory, *, 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_mu, nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) @@ -458,7 +459,6 @@ class StokesletIdentity: """[Pozrikidis1992] Problem 3.1.1""" def __init__(self, ambient_dim): - from pytential.symbolic.stokes import StokesletWrapper self.ambient_dim = ambient_dim self.stokeslet = StokesletWrapper(self.ambient_dim, mu_sym=1) @@ -517,7 +517,6 @@ class StressletIdentity: """[Pozrikidis1992] Equation 3.2.7""" def __init__(self, ambient_dim): - from pytential.symbolic.stokes import StokesletWrapper self.ambient_dim = ambient_dim self.stokeslet = StokesletWrapper(self.ambient_dim, mu_sym=1) @@ -570,6 +569,116 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): # }}} +# {{{ test Stokes PDE + +class StokesPDE: + def __init__(self, ambient_dim, operator): + self.ambient_dim = ambient_dim + self.operator = operator + + def apply_operator(self): + dim = self.ambient_dim + args = { + "density_vec_sym": [1]*dim, + "qbx_forced_limit": 1, + } + if isinstance(self.operator, StressletWrapperBase): + args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() + + dd_u = [self.operator.apply(**args, extra_deriv_dirs=(i, i)) + for i in range(dim)] + laplace_u = [sum(dd_u[j][i] for j in range(dim)) for i in range(dim)] + d_p = [self.operator.apply_pressure(**args, extra_deriv_dirs=(i,)) + for i in range(dim)] + res = make_obj_array([laplace_u[i] - d_p[i] for i in range(dim)]) + return res + + def ref_result(self): + return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) + + +@pytest.mark.parametrize("dim, method", [ + (2, "naive"), + (2, "biharmonic"), + (3, "naive"), + (3, "biharmonic"), + (3, "laplace"), + ]) +def test_stokeslet_pde(actx_factory, dim, method, visualize=False): + if visualize: + logging.basicConfig(level=logging.INFO) + + if dim == 2: + case_cls = eid.StarfishTestCase + resolutions = [16, 32, 64, 96, 128] + else: + case_cls = eid.SpheroidTestCase + resolutions = [0, 1, 2] + + source_ovsmp = 4 if dim == 2 else 8 + case = case_cls(fmm_backend=None, + target_order=5, qbx_order=3, source_ovsmp=source_ovsmp, + resolutions=resolutions) + identity = StokesPDE(dim, + StokesletWrapper(case.ambient_dim, mu_sym=1, method=method)) + + for resolution in resolutions: + h_max, errors = run_stokes_identity( + actx_factory, case, identity, + resolution=resolution, + visualize=visualize) + + +@pytest.mark.parametrize("dim, method", [ + (2, "naive"), + (2, "biharmonic"), + (3, "naive"), + (3, "biharmonic"), + (3, "laplace"), + ]) +def test_stresslet_pde(actx_factory, dim, method, visualize=False): + if visualize: + logging.basicConfig(level=logging.INFO) + + if dim == 2: + case_cls = eid.StarfishTestCase + resolutions = [16, 32, 64, 96, 128] + else: + case_cls = eid.SpheroidTestCase + resolutions = [0, 1, 2] + + source_ovsmp = 4 if dim == 2 else 8 + case = case_cls(fmm_backend=None, + target_order=5, qbx_order=3, source_ovsmp=source_ovsmp, + resolutions=resolutions) + identity = StokesPDE(dim, + StressletWrapper(case.ambient_dim, mu_sym=1, method=method)) + + from pytools.convergence import EOCRecorder + eocs = [EOCRecorder() for _ in range(case.ambient_dim)] + + for resolution in case.resolutions: + h_max, errors = run_stokes_identity( + actx_factory, case, identity, + resolution=resolution, + visualize=visualize) + + for eoc, e in zip(eocs, errors): + eoc.add_data_point(h_max, e) + + for eoc in eocs: + print(eoc.pretty_print( + abscissa_format="%.8e", + error_format="%.8e", + eoc_format="%.2f")) + + for eoc in eocs: + order = min(case.target_order, case.qbx_order) + assert eoc.order_estimate() > order - 1.0 + +# }}} + + # You can test individual routines by typing # $ python test_stokes.py 'test_routine()' From edc6d9044377d0d3132c5be06b9aa818bfb00da3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 7 Sep 2022 17:14:26 -0500 Subject: [PATCH 023/156] disable biharmonic 3D --- test/test_stokes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 17b7d383a..dcd0bfd94 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -633,7 +633,8 @@ def test_stokeslet_pde(actx_factory, dim, method, visualize=False): (2, "naive"), (2, "biharmonic"), (3, "naive"), - (3, "biharmonic"), + # FIXME: re-enable when merge_int_g_exprs is in + # (3, "biharmonic"), (3, "laplace"), ]) def test_stresslet_pde(actx_factory, dim, method, visualize=False): @@ -674,7 +675,7 @@ def test_stresslet_pde(actx_factory, dim, method, visualize=False): for eoc in eocs: order = min(case.target_order, case.qbx_order) - assert eoc.order_estimate() > order - 1.0 + assert eoc.order_estimate() > order - 1.5 # }}} From 7afaf0dcc8bd17d8bf5284922268de341d3b660e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 7 Sep 2022 17:15:22 -0500 Subject: [PATCH 024/156] update warning message --- 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 9174da9f1..65b3c48e7 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -642,8 +642,8 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, def StokesletWrapper(dim, 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.") + 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) @@ -665,8 +665,8 @@ def StokesletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): def StressletWrapper(dim, 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.") + 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) From d397563d0bcfb0230b8918310f46299e1e5aac5d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 7 Sep 2022 23:44:47 -0500 Subject: [PATCH 025/156] raise NotImplmentedError when base_kernel is not given --- pytential/symbolic/pde/system_utils.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 4630b862d..78c63c8a9 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -71,12 +71,15 @@ def rewrite_using_base_kernel(exprs: List[ExpressionT], If *base_kernel* is *None*, the expression list is returned as is. If *base_kernel* is not given, the method will try to find a base kernel. + This is currently not implemented and will raise a ``NotImplementedError``. - The routine will fail with a ``RuntimeError`` if this process cannot be - completed. + The routine will fail with a ``RuntimeError`` when *base_kernel* is given, but + method fails to find a way to rewrite. """ if base_kernel is None: return list(exprs) + if base_kernel == _NO_ARG_SENTINEL: + raise NotImplementedError mapper = RewriteUsingBaseKernelMapper(base_kernel) return [mapper(expr) for expr in exprs] @@ -97,15 +100,10 @@ def map_int_g(self, expr): # Convert IntGs with TargetPointMultiplier/AxisTargetDerivative to a sum of # IntGs without TargetPointMultipliers new_int_gs = convert_target_transformation_to_source(expr) - # if base_kernel was not given, use the first we find - if self.base_kernel == _NO_ARG_SENTINEL: - self.base_kernel = int_g.target_kernel.get_base_kernel() - return int_g - else: - # 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) + # 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 _get_kernel_expression(expr: ExpressionT, From d66bd643d092496de6c5d4e69aeb656cb4bc0f33 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 00:11:35 -0500 Subject: [PATCH 026/156] laplace works just fine for 2D too --- pytential/symbolic/stokes.py | 10 ++-------- test/test_stokes.py | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 65b3c48e7..934bbfb97 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -531,9 +531,6 @@ class StokesletWrapperTornberg(StokesletWrapperBase): 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) @@ -541,7 +538,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.nu = nu_sym def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): - stresslet = StressletWrapperTornberg(3, self.mu, self.nu) + stresslet = StressletWrapperTornberg(self.dim, 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) @@ -555,11 +552,8 @@ class StressletWrapperTornberg(StressletWrapperBase): 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): + def __init__(self, dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.dim = dim - if dim != 3: - 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) diff --git a/test/test_stokes.py b/test/test_stokes.py index dcd0bfd94..f07250d0b 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -598,11 +598,12 @@ def ref_result(self): @pytest.mark.parametrize("dim, method", [ + (2, "laplace"), (2, "naive"), (2, "biharmonic"), + (3, "laplace"), (3, "naive"), (3, "biharmonic"), - (3, "laplace"), ]) def test_stokeslet_pde(actx_factory, dim, method, visualize=False): if visualize: @@ -630,6 +631,7 @@ def test_stokeslet_pde(actx_factory, dim, method, visualize=False): @pytest.mark.parametrize("dim, method", [ + (2, "laplace"), (2, "naive"), (2, "biharmonic"), (3, "naive"), From d62e336eef02c5f8920714a4fee0cdbf54c53494 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 00:25:28 -0500 Subject: [PATCH 027/156] check divergence too --- test/test_stokes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index f07250d0b..15be7b9c8 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -585,13 +585,15 @@ def apply_operator(self): if isinstance(self.operator, StressletWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() + d_u = [self.operator.apply(**args, extra_deriv_dirs=(i,)) + for i in range(dim)] dd_u = [self.operator.apply(**args, extra_deriv_dirs=(i, i)) for i in range(dim)] laplace_u = [sum(dd_u[j][i] for j in range(dim)) for i in range(dim)] d_p = [self.operator.apply_pressure(**args, extra_deriv_dirs=(i,)) for i in range(dim)] - res = make_obj_array([laplace_u[i] - d_p[i] for i in range(dim)]) - return res + eqs = [laplace_u[i] - d_p[i] for i in range(dim)] + [sum(d_u)] + return make_obj_array(eqs) def ref_result(self): return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) From 95c06f0cbec7d862f8bfa59b8d2b015be3cb827f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 00:40:21 -0500 Subject: [PATCH 028/156] test laplace 2d too --- test/test_stokes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_stokes.py b/test/test_stokes.py index 15be7b9c8..e44cd0e11 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -307,6 +307,7 @@ def print_timing_data(timings, name): @pytest.mark.parametrize("ambient_dim, method, nu", [ (2, "naive", 0.5), + (2, "laplace", 0.5), (2, "biharmonic", 0.5), pytest.param(3, "naive", 0.5, marks=pytest.mark.slowtest), pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.slowtest), From d7cd503185caccd17ea1a1b9db483cf4aed69b5a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 01:47:26 -0500 Subject: [PATCH 029/156] remove pylint skips now that __new__ is not used anymore --- pytential/symbolic/stokes.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 934bbfb97..5cdb5cb29 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -352,13 +352,10 @@ 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 @@ -797,13 +794,10 @@ 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) - # 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): @@ -817,12 +811,9 @@ 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 @@ -876,13 +867,10 @@ 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): - # 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"): From 2658f8b1e41dc775d1d543e1eefeeadf4864dcb1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 17:23:33 -0500 Subject: [PATCH 030/156] test elasticity too --- test/test_stokes.py | 106 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 20 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index e44cd0e11..d441e10a6 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -600,15 +600,62 @@ def ref_result(self): return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) -@pytest.mark.parametrize("dim, method", [ - (2, "laplace"), - (2, "naive"), - (2, "biharmonic"), - (3, "laplace"), - (3, "naive"), - (3, "biharmonic"), +class ElasticityPDE: + def __init__(self, ambient_dim, operator): + self.ambient_dim = ambient_dim + self.operator = operator + + def apply_operator(self): + dim = self.ambient_dim + args = { + "density_vec_sym": [1]*dim, + "qbx_forced_limit": 1, + } + if isinstance(self.operator, StressletWrapperBase): + args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() + + mu = self.operator.mu + nu = self.operator.nu + assert nu != 0.5 + lam = 2*nu*mu/(1-2*nu) + + derivs = {} + + for i in range(dim): + for j in range(i + 1): + derivs[(i, j)] = self.operator.apply(**args, + extra_deriv_dirs=(i, j)) + derivs[(j, i)] = derivs[(i, j)] + + laplace_u = sum(derivs[(i, i)] for i in range(dim)) + grad_of_div_u = [sum(derivs[(i, j)][j] for j in range(dim)) + for i in range(dim)] + + # Navier-Cauchy equations + eqs = [(lam + mu)*grad_of_div_u[i] + mu*laplace_u[i] for i in range(dim)] + return make_obj_array(eqs) + + def ref_result(self): + return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) + + +@pytest.mark.parametrize("dim, method, nu", [ + pytest.param(2, "biharmonic", 0.4), + pytest.param(2, "biharmonic", 0.5), + pytest.param(2, "laplace", 0.5), + pytest.param(3, "laplace", 0.5), + pytest.param(3, "laplace", 0.4), + pytest.param(2, "naive", 0.4, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.4, marks=pytest.mark.slowtest), + pytest.param(2, "naive", 0.5, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.5, marks=pytest.mark.slowtest), + # FIXME: re-enable when merge_int_g_exprs is in + pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.skip), + # FIXME: re-enable when implemented + pytest.param(2, "laplace", 0.4, marks=pytest.mark.xfail), ]) -def test_stokeslet_pde(actx_factory, dim, method, visualize=False): +def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): if visualize: logging.basicConfig(level=logging.INFO) @@ -623,8 +670,14 @@ def test_stokeslet_pde(actx_factory, dim, method, visualize=False): case = case_cls(fmm_backend=None, target_order=5, qbx_order=3, source_ovsmp=source_ovsmp, resolutions=resolutions) - identity = StokesPDE(dim, - StokesletWrapper(case.ambient_dim, mu_sym=1, method=method)) + + if nu == 0.5: + pde_class = StokesPDE + else: + pde_class = ElasticityPDE + + identity = pde_class(dim, + StokesletWrapper(case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) for resolution in resolutions: h_max, errors = run_stokes_identity( @@ -633,16 +686,23 @@ def test_stokeslet_pde(actx_factory, dim, method, visualize=False): visualize=visualize) -@pytest.mark.parametrize("dim, method", [ - (2, "laplace"), - (2, "naive"), - (2, "biharmonic"), - (3, "naive"), +@pytest.mark.parametrize("dim, method, nu", [ + pytest.param(2, "laplace", 0.5), + pytest.param(3, "laplace", 0.5), + pytest.param(3, "laplace", 0.4), + pytest.param(2, "naive", 0.4, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.4, marks=pytest.mark.slowtest), + pytest.param(2, "naive", 0.5, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.5, marks=pytest.mark.slowtest), # FIXME: re-enable when merge_int_g_exprs is in - # (3, "biharmonic"), - (3, "laplace"), + pytest.param(2, "biharmonic", 0.4, marks=pytest.mark.skip), + pytest.param(2, "biharmonic", 0.5, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.skip), + # FIXME: re-enable when implemented + pytest.param(2, "laplace", 0.4, marks=pytest.mark.xfail), ]) -def test_stresslet_pde(actx_factory, dim, method, visualize=False): +def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): if visualize: logging.basicConfig(level=logging.INFO) @@ -657,8 +717,14 @@ def test_stresslet_pde(actx_factory, dim, method, visualize=False): case = case_cls(fmm_backend=None, target_order=5, qbx_order=3, source_ovsmp=source_ovsmp, resolutions=resolutions) - identity = StokesPDE(dim, - StressletWrapper(case.ambient_dim, mu_sym=1, method=method)) + + if nu == 0.5: + pde_class = StokesPDE + else: + pde_class = ElasticityPDE + + identity = pde_class(dim, + StressletWrapper(case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) from pytools.convergence import EOCRecorder eocs = [EOCRecorder() for _ in range(case.ambient_dim)] From 216930c54232917b6f3f8285e332a6f2f63c386e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 17:41:27 -0500 Subject: [PATCH 031/156] Update docs and remove unnecessary for loop --- pytential/symbolic/pde/system_utils.py | 100 ++++++++++++++----------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 78c63c8a9..7c33151a9 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -37,7 +37,7 @@ from pytential.utils import chop, lu_solve_with_expand import pytential -from typing import List, Mapping, Text, Any, Union, Tuple +from typing import List, Mapping, Text, Any, Union, Tuple, Optional from pytential.symbolic.typing import ExpressionT import logging @@ -274,6 +274,9 @@ def _multiply_int_g(int_g: IntG, expr_multiplier: sym.Basic, def convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ -> ExpressionT: + """Converts an *IntG* to an expression with *IntG*s having the + base kernel *base_kernel*. + """ result = 0 for knl, density in zip(int_g.source_kernels, int_g.densities): result += _convert_int_g_to_base( @@ -284,46 +287,51 @@ def convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ -> ExpressionT: + """Converts an *IntG* with only one source kernel to an expression with *IntG*s + having the base kernel *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, 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 - # 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 * c,), kernel_arguments=new_kernel_args) + + density, = int_g.densities + source_kernel, = int_g.source_kernels + deriv_relation = get_deriv_relation_kernel(source_kernel.get_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 + # 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 * c,), kernel_arguments=new_kernel_args) return result @@ -331,8 +339,8 @@ def get_deriv_relation(kernels: List[ExpressionKernel], base_kernel: ExpressionKernel, kernel_arguments: Mapping[Text, Any], tol: float = 1e-10, - order: Union[None, int] = None) \ - -> List[Tuple[ExpressionT, ExpressionT]]: + order: Optional[int] = None) \ + -> List[Tuple[ExpressionT, List[Tuple[Tuple[int], ExpressionT]]]]: res = [] for knl in kernels: res.append(get_deriv_relation_kernel(knl, base_kernel, @@ -346,8 +354,16 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, base_kernel: ExpressionKernel, hashable_kernel_arguments: Tuple[Tuple[Text, Any]], tol: float = 1e-10, - order: Union[None, int] = None) \ - -> Tuple[ExpressionT, ExpressionT]: + order: Optional[int] = None) \ + -> Tuple[ExpressionT, List[Tuple[Tuple[int], ExpressionT]]]: + """Takes a *kernel* and a base_kernel* as input and re-writes the + *kernel* as a linear combination of derivatives of *base_kernel* up-to + order *order* and a constant. *tol* is an upper limit for small numbers that + are replaced with zero in the numerical procedure. + + Returns the constant and a list of (mulit-index, coeff) to represent the + linear combination of derivatives + """ kernel_arguments = dict(hashable_kernel_arguments) (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, hashable_kernel_arguments=hashable_kernel_arguments) @@ -387,7 +403,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, @memoize_on_first_arg def _get_base_kernel_matrix(base_kernel: ExpressionKernel, hashable_kernel_arguments: Tuple[Tuple[Text, Any]], - order: Union[None, int] = None, retries: int = 3) \ + order: Optional[int] = None, retries: int = 3) \ -> Tuple[Tuple[sym.Matrix, sym.Matrix, List[Tuple[int, int]]], np.ndarray, List[Tuple[int]]]: dim = base_kernel.dim From 53ac31d61f225d5a6c4bad0866bada5226f7d0d6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 17:49:06 -0500 Subject: [PATCH 032/156] no need of laplace kernel when nu == 0.5 --- pytential/symbolic/stokes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 5cdb5cb29..3a0ff446b 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -406,7 +406,8 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, 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 nu_sym != 0.5: + self.kernel_dict["laplace"] = LaplaceKernel(self.dim) def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): @@ -432,6 +433,8 @@ def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ zip(kernel_indices, dir_vec_indices, coeffs, extra_deriv_dirs_vec): + if coeff == 0: + continue 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], From e0fc9ca9a52166387cf65f899b53bc46137bb42e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 8 Sep 2022 17:55:28 -0500 Subject: [PATCH 033/156] update copyright --- pytential/symbolic/stokes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 3a0ff446b..e26ebf6c8 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -1,4 +1,7 @@ -__copyright__ = "Copyright (C) 2017 Natalie Beams" +__copyright__ = """ +Copyright (C) 2017 Natalie Beams +Copyright (C) 2022 Isuru Fernando +""" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy From 7b3009a37adb581f0f378d3798028bb1ff73cecf Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 11:35:04 -0500 Subject: [PATCH 034/156] store the stresslet in StokesletWrapperYoshida --- pytential/symbolic/elasticity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 64e6e766b..8a3d99495 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -158,9 +158,9 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): self.kernel = LaplaceKernel(dim=3) self.mu = mu_sym self.nu = nu_sym + self.stresslet = StressletWrapperYoshida(3, self.mu, self.nu) 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, + return self.stresslet.apply_stokeslet_and_stresslet(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) From a05bcad40c24225ad258d585d80038c31de792c3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 12:41:09 -0500 Subject: [PATCH 035/156] nu default --- pytential/symbolic/elasticity.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 8a3d99495..7a3d239cc 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -27,6 +27,10 @@ TargetPointMultiplier, LaplaceKernel) from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase, _MU_SYM_DEFAULT) +from sumpy.symbolic import SpatialConstant + + +_NU_SYM_DEFAULT = SpatialConstant("nu") class StressletWrapperYoshida(StressletWrapperBase): @@ -39,7 +43,7 @@ class StressletWrapperYoshida(StressletWrapperBase): International Journal for Numerical Methods in Engineering, 50(3), 525-547. """ - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " @@ -150,7 +154,7 @@ class StokesletWrapperYoshida(StokesletWrapperBase): International Journal for Numerical Methods in Engineering, 50(3), 525-547. """ - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " From 55b81aa08c58ff846a3a245850dff380bef5a9b5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:39:23 -0500 Subject: [PATCH 036/156] Use sym.SympyToPymbolicMapper --- pytential/symbolic/pde/system_utils.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 7c33151a9..825ea99ae 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -22,7 +22,6 @@ import numpy as np -from sumpy.symbolic import make_sym_vector, SympyToPymbolicMapper import sumpy.symbolic as sym import pymbolic from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, @@ -148,8 +147,7 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: IntG(x*r, sigma) -> [IntG(r, sigma*y), IntG(r*(x -y), sigma)] """ import sympy - import sumpy.symbolic as sym - from sumpy.symbolic import SympyToPymbolicMapper + from sumpy.symbolic.sympy import SympyToPymbolicMapper conv = SympyToPymbolicMapper() knl = int_g.target_kernel @@ -240,16 +238,15 @@ def _multiply_int_g(int_g: IntG, expr_multiplier: sym.Basic, 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) + sym_d = sym.make_sym_vector("d", base_kernel.dim) base_kernel_expr = _get_kernel_expression(base_kernel.expression, int_g.kernel_arguments) subst = {pymbolic.var(f"d{i}"): pymbolic.var("d")[i] for i in range(base_kernel.dim)} - conv = SympyToPymbolicMapper() + conv = sym.SympyToPymbolicMapper() if expr_multiplier == 1: # if there's no expr_multiplier, only multiply the densities @@ -368,8 +365,8 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, hashable_kernel_arguments=hashable_kernel_arguments) dim = base_kernel.dim - sym_vec = make_sym_vector("d", dim) - sympy_conv = SympyToPymbolicMapper() + sym_vec = sym.make_sym_vector("d", dim) + sympy_conv = sym.SympyToPymbolicMapper() expr = _get_kernel_expression(kernel.expression, kernel_arguments) vec = [] @@ -431,7 +428,7 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, 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) + sym_vec = sym.make_sym_vector("d", dim) base_expr = _get_kernel_expression(base_kernel.expression, dict(hashable_kernel_arguments)) @@ -519,9 +516,9 @@ def filter_kernel_arguments(knls, kernel_arguments): get_deriv_relation(kernels, base_kernel, tol=1e-10, order=4, kernel_arguments={}) base_kernel = BiharmonicKernel(3) - sym_d = make_sym_vector("d", base_kernel.dim) + sym_d = sym.make_sym_vector("d", base_kernel.dim) sym_r = sym.sqrt(sum(a**2 for a in sym_d)) - conv = SympyToPymbolicMapper() + conv = sym.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) From 1b1ce35e0f97504aabaafb649e7c72bc4bf7cc46 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:40:05 -0500 Subject: [PATCH 037/156] consistent spacing --- 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 825ea99ae..250ad7a28 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -27,8 +27,8 @@ from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, ExpressionKernel, KernelWrapper, TargetPointMultiplier) from pytools import (memoize_on_first_arg, - generate_nonnegative_integer_tuples_summing_to_at_most - as gnitstam) + generate_nonnegative_integer_tuples_summing_to_at_most + as gnitstam) from pytential.symbolic.primitives import (NodeCoordinateComponent, hashable_kernel_args, IntG) From e24feaf0354bdc9032c943a934c510ae7dd93234 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:41:35 -0500 Subject: [PATCH 038/156] fix typo --- 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 250ad7a28..cd8c7e89a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -108,7 +108,7 @@ def map_int_g(self, expr): def _get_kernel_expression(expr: ExpressionT, kernel_arguments: Mapping[Text, Any]) -> sym.Basic: """Convert a :mod:`pymbolic` expression to :mod:`sympy` expression - after susituting kernel arguments. + after substituting kernel arguments. For eg: `exp(I*k*r)/r` with `{k: 1}` is converted to the sympy expression `exp(I*r)/r` From 6474354a1def067c42dfdd02619639f44fcf2dee Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:42:46 -0500 Subject: [PATCH 039/156] _get_kernel_expression -> _get_sympy_kernel_expression --- pytential/symbolic/pde/system_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index cd8c7e89a..8c34214aa 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -105,7 +105,7 @@ def map_int_g(self, expr): self.base_kernel) for new_int_g in new_int_gs) -def _get_kernel_expression(expr: ExpressionT, +def _get_sympy_kernel_expression(expr: ExpressionT, kernel_arguments: Mapping[Text, Any]) -> sym.Basic: """Convert a :mod:`pymbolic` expression to :mod:`sympy` expression after substituting kernel arguments. @@ -242,7 +242,7 @@ def _multiply_int_g(int_g: IntG, expr_multiplier: sym.Basic, base_kernel = int_g.target_kernel.get_base_kernel() sym_d = sym.make_sym_vector("d", base_kernel.dim) - base_kernel_expr = _get_kernel_expression(base_kernel.expression, + base_kernel_expr = _get_sympy_kernel_expression(base_kernel.expression, int_g.kernel_arguments) subst = {pymbolic.var(f"d{i}"): pymbolic.var("d")[i] for i in range(base_kernel.dim)} @@ -368,7 +368,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, sym_vec = sym.make_sym_vector("d", dim) sympy_conv = sym.SympyToPymbolicMapper() - expr = _get_kernel_expression(kernel.expression, kernel_arguments) + expr = _get_sympy_kernel_expression(kernel.expression, kernel_arguments) vec = [] for i in range(len(mis)): vec.append(evalf(expr.xreplace(dict((k, v) for @@ -384,14 +384,14 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, if coeff == 0: continue if mis[i] != (-1, -1, -1): - coeff *= _get_kernel_expression(kernel.global_scaling_const, + coeff *= _get_sympy_kernel_expression(kernel.global_scaling_const, kernel_arguments) - coeff /= _get_kernel_expression(base_kernel.global_scaling_const, + coeff /= _get_sympy_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 * _get_kernel_expression( + const = sympy_conv(coeff * _get_sympy_kernel_expression( kernel.global_scaling_const, kernel_arguments)) logger.debug(" + %s", const) return (const, result) @@ -430,7 +430,7 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, rand[i, j] = sym.sympify(rand[i, j])/10**15 sym_vec = sym.make_sym_vector("d", dim) - base_expr = _get_kernel_expression(base_kernel.expression, + base_expr = _get_sympy_kernel_expression(base_kernel.expression, dict(hashable_kernel_arguments)) mat = [] From 580045c41628a5cb40507abb565d0fefe55023c8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:44:52 -0500 Subject: [PATCH 040/156] improved docstring --- pytential/symbolic/pde/system_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 8c34214aa..508b0dcc7 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -235,8 +235,9 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: def _multiply_int_g(int_g: IntG, expr_multiplier: sym.Basic, density_multiplier: ExpressionT) -> List[IntG]: """Multiply the expression in ``IntG`` with the *expr_multiplier* - which is a symbolic expression and multiply the densities - with *density_multiplier* which is a pymbolic expression. + which is a symbolic (:mod:`sympy` or :mod:`symengine`) expression and + multiply the densities with *density_multiplier* which is a :mod:`pymbolic` + expression. """ result = [] From c93371d638862ba939e29a965d317b9a1b9173f8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:46:08 -0500 Subject: [PATCH 041/156] Fix tuple types --- 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 508b0dcc7..bb9c717e0 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -350,10 +350,10 @@ def get_deriv_relation(kernels: List[ExpressionKernel], @memoize_on_first_arg def get_deriv_relation_kernel(kernel: ExpressionKernel, base_kernel: ExpressionKernel, - hashable_kernel_arguments: Tuple[Tuple[Text, Any]], + hashable_kernel_arguments: Tuple[Tuple[Text, Any], ...], tol: float = 1e-10, order: Optional[int] = None) \ - -> Tuple[ExpressionT, List[Tuple[Tuple[int], ExpressionT]]]: + -> Tuple[ExpressionT, List[Tuple[Tuple[int, ...], ExpressionT]]]: """Takes a *kernel* and a base_kernel* as input and re-writes the *kernel* as a linear combination of derivatives of *base_kernel* up-to order *order* and a constant. *tol* is an upper limit for small numbers that @@ -400,10 +400,10 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, @memoize_on_first_arg def _get_base_kernel_matrix(base_kernel: ExpressionKernel, - hashable_kernel_arguments: Tuple[Tuple[Text, Any]], + hashable_kernel_arguments: Tuple[Tuple[Text, Any], ...], order: Optional[int] = None, retries: int = 3) \ -> Tuple[Tuple[sym.Matrix, sym.Matrix, List[Tuple[int, int]]], - np.ndarray, List[Tuple[int]]]: + np.ndarray, List[Tuple[int, ...]]]: dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() From d803468b8b7c3628b4736b1d9cac9a3f06e15e6d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:48:34 -0500 Subject: [PATCH 042/156] improve docstring --- pytential/symbolic/pde/system_utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index bb9c717e0..59a66c9eb 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -137,11 +137,12 @@ def _monom_to_expr(monom: List[int], def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: - """Convert an ``IntG`` with AxisTargetDerivative/TargetMultiplier to a list + """Convert an ``IntG`` with :class:`sumpy.kernel.AxisTargetDerivative` + or :class:`sumpy.kernel.TargetMultiplier` to a list of ``IntG``s without them and only source dependent transformations. - The sum of the list returned is a tranformation of the input ``IntG``. + The sum of the list returned is equivalent to the input *int_g*. - eg:: + For example:: IntG(d/dx r, sigma) -> [IntG(d/dy r, -sigma)] IntG(x*r, sigma) -> [IntG(r, sigma*y), IntG(r*(x -y), sigma)] @@ -359,8 +360,8 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, order *order* and a constant. *tol* is an upper limit for small numbers that are replaced with zero in the numerical procedure. - Returns the constant and a list of (mulit-index, coeff) to represent the - linear combination of derivatives + :returns: the constant and a list of (multi-index, coeff) to represent the + linear combination of derivatives """ kernel_arguments = dict(hashable_kernel_arguments) (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, From e16a4c0e19314eed2bad8b70ca4d64c0fc54d899 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:53:00 -0500 Subject: [PATCH 043/156] pep8 naming --- pytential/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/utils.py b/pytential/utils.py index 1cf615c78..68446b3c7 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -82,13 +82,13 @@ def backward_substitution(U, b): res[i] = (res[i] / U[i, i]).expand() return res - def permuteFwd(b, perm): + def permute_fwd(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))) + forward_substitution(L, permute_fwd(b, perm))) # vim: foldmethod=marker From a4178cd0831e05fd3e80a9b98aea71069a6f4c39 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 17:54:37 -0500 Subject: [PATCH 044/156] explain 2D StokesletWrapperYoshida --- 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 d441e10a6..88dc3e60d 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -652,7 +652,7 @@ def ref_result(self): # FIXME: re-enable when merge_int_g_exprs is in pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.skip), pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.skip), - # FIXME: re-enable when implemented + # FIXME: re-enable when StokesletWrapperYoshida is implemented for 2D pytest.param(2, "laplace", 0.4, marks=pytest.mark.xfail), ]) def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): @@ -699,7 +699,7 @@ def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): pytest.param(2, "biharmonic", 0.5, marks=pytest.mark.skip), pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.skip), pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.skip), - # FIXME: re-enable when implemented + # FIXME: re-enable when StressletWrapperYoshida is implemented for 2D pytest.param(2, "laplace", 0.4, marks=pytest.mark.xfail), ]) def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): From 54bc8486d841b13c152809f06499bd311503bb6d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 19:00:57 -0500 Subject: [PATCH 045/156] Fix typo --- 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 59a66c9eb..94a0439d9 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -148,7 +148,7 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: IntG(x*r, sigma) -> [IntG(r, sigma*y), IntG(r*(x -y), sigma)] """ import sympy - from sumpy.symbolic.sympy import SympyToPymbolicMapper + from pymbolic.interop.sympy import SympyToPymbolicMapper conv = SympyToPymbolicMapper() knl = int_g.target_kernel From a9893f9ecf822b46b52259848e5dcc3384a942e5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 20:09:44 -0500 Subject: [PATCH 046/156] check that the kernel is a derivative when const != 0 --- pytential/symbolic/pde/system_utils.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 94a0439d9..5fd9acf86 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -25,7 +25,8 @@ import sumpy.symbolic as sym import pymbolic from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, - ExpressionKernel, KernelWrapper, TargetPointMultiplier) + ExpressionKernel, KernelWrapper, TargetPointMultiplier, + DirectionalSourceDerivative) from pytools import (memoize_on_first_arg, generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam) @@ -313,9 +314,14 @@ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ # 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. + if const != 0 and source_kernel != source_kernel.get_base_kernel(): + # We only handle the case where any source transformation is a derivative + # and the constant when applied becomes zero. We bail out if not + knl = source_kernel + while isinstance(knl, KernelWrapper): + if not isinstance(knl, (AxisSourceDerivative, DirectionalSourceDerivative)): + return int_g + knl = knl.inner_kernel const = 0 result += const From 0f59312d2f501992b84e369767e289d520ab25a8 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 20:13:48 -0500 Subject: [PATCH 047/156] Add DOI link --- pytential/symbolic/elasticity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 7a3d239cc..b7f0654b7 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -41,6 +41,7 @@ class StressletWrapperYoshida(StressletWrapperBase): fast multipole Galerkin boundary integral equation method to elastostatic crack problems in 3D. International Journal for Numerical Methods in Engineering, 50(3), 525-547. + https://doi.org/10.1002/1097-0207(20010130)50:3<525::AID-NME34>3.0.CO;2-4 """ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): @@ -152,6 +153,7 @@ class StokesletWrapperYoshida(StokesletWrapperBase): fast multipole Galerkin boundary integral equation method to elastostatic crack problems in 3D. International Journal for Numerical Methods in Engineering, 50(3), 525-547. + https://doi.org/10.1002/1097-0207(20010130)50:3<525::AID-NME34>3.0.CO;2-4 """ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): From 8ccd3430b58978e08dd674db181c9cb36900d3da Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 20:22:02 -0500 Subject: [PATCH 048/156] Fix formatting --- 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 5fd9acf86..08c660f14 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -319,7 +319,8 @@ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ # and the constant when applied becomes zero. We bail out if not knl = source_kernel while isinstance(knl, KernelWrapper): - if not isinstance(knl, (AxisSourceDerivative, DirectionalSourceDerivative)): + if not isinstance(knl, + (AxisSourceDerivative, DirectionalSourceDerivative)): return int_g knl = knl.inner_kernel const = 0 From 1331e9e79873a0c342e902e28ea525423de8121d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 12 Sep 2022 23:24:57 -0500 Subject: [PATCH 049/156] Make Stokes be a child class of Elasticity --- pytential/symbolic/elasticity.py | 428 ++++++++++++++++++++++++++-- pytential/symbolic/stokes.py | 475 ++++++------------------------- test/test_stokes.py | 32 ++- 3 files changed, 515 insertions(+), 420 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index b7f0654b7..7fd3f92da 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -1,4 +1,7 @@ -__copyright__ = "Copyright (C) 2021 Isuru Fernando" +__copyright__ = """ +Copyright (C) 2017 Natalie Beams +Copyright (C) 2022 Isuru Fernando +""" __license__ = """ Permission is hereby granted, free of charge, to any person obtaining a copy @@ -23,19 +26,410 @@ import numpy as np from pytential import sym -from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, - TargetPointMultiplier, LaplaceKernel) -from pytential.symbolic.stokes import (StressletWrapperBase, StokesletWrapperBase, - _MU_SYM_DEFAULT) +from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel +from sumpy.kernel import (StressletKernel, LaplaceKernel, + ElasticityKernel, BiharmonicKernel, + AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant +from abc import ABC, abstractmethod + +__doc__ = """ +.. autoclass:: ElasticityWrapper +.. autoclass:: ElasticityDoubleLayerWrapper +""" + +# {{{ ElasiticityWrapper ABCs +_MU_SYM_DEFAULT = SpatialConstant("mu") _NU_SYM_DEFAULT = SpatialConstant("nu") -class StressletWrapperYoshida(StressletWrapperBase): - """Stresslet Wrapper using Yoshida et al's method [1] which uses Laplace - derivatives. +class ElasticityWrapperBase(ABC): + """Wrapper class for the :class:`~sumpy.kernel.ElasticityKernel` kernel. + + This class is meant to shield the user from the messiness of writing + out every term in the expansion of the double-indexed Elasticity 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_derivative` (target derivative). + + .. automethod:: apply + .. automethod:: apply_derivative + """ + def __init__(self, dim, mu_sym, nu_sym): + self.dim = dim + self.mu = mu_sym + self.nu = nu_sym + + @abstractmethod + 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 + 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 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 + + 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,)) + + +class ElasticityDoubleLayerWrapperBase(ABC): + """Wrapper class for the double layer of + :class:`~sumpy.kernel.ElasticityKernel` 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_derivative` (target derivative). + + .. automethod:: apply + .. automethod:: apply_derivative + """ + def __init__(self, dim, mu_sym, nu_sym): + self.dim = dim + self.mu = mu_sym + self.nu = nu_sym + + @abstractmethod + 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 + 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 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 + + 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 + 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 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, dir_vec_sym, qbx_forced_limit, + (deriv_dir,)) + +# }}} + + +# {{{ Naive and Biharmonic impl + +def _create_int_g(knl, deriv_dirs, density, **kwargs): + for deriv_dir in deriv_dirs: + knl = AxisTargetDerivative(deriv_dir, knl) + + 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) + + res = sym.int_g_vec(knl, density, **kwargs) + return res + + +class _ElasticityWrapperNaiveOrBiharmonic(ElasticityWrapperBase): + def __init__(self, dim, mu_sym, nu_sym, base_kernel): + super().__init__(dim, mu_sym, nu_sym) + if not (dim == 3 or dim == 2): + raise ValueError("unsupported dimension given to ElasticityWrapper") + + self.base_kernel = base_kernel + + 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 + + # 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, dim): + self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, + jcomp=j, poisson_ratio=poisson_ratio) + self.kernel_dict[(j, i)] = self.kernel_dict[(i, j)] + + def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, + deriv_dirs): + """ + Returns the Integral of the elasticity kernel given by `idx` + and its derivatives. + """ + res = _create_int_g(self.kernel_dict[idx], deriv_dirs, + density=density_sym*dir_vec_sym[idx[-1]], + 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=()): + + 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], [1]*self.dim, + qbx_forced_limit, deriv_dirs=extra_deriv_dirs) + + return np.array(rewrite_using_base_kernel(sym_expr, + base_kernel=self.base_kernel)) + + +class ElasticityWrapperNaive(_ElasticityWrapperNaiveOrBiharmonic): + def __init__(self, dim, mu_sym, nu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, base_kernel=None) + + +class ElasticityWrapperBiharmonic(_ElasticityWrapperNaiveOrBiharmonic): + def __init__(self, dim, mu_sym, nu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + base_kernel=BiharmonicKernel(dim)) + + +# }}} + + +# {{{ ElasticityDoubleLayerWrapper Naive and Biharmonic impl + +class _ElasticityDoubleLayerWrapperNaiveOrBiharmonic( + ElasticityDoubleLayerWrapperBase): + + def __init__(self, dim, mu_sym, nu_sym, base_kernel): + super().__init__(dim, mu_sym, nu_sym) + if not (dim == 3 or dim == 2): + raise ValueError("unsupported dimension given to " + "ElasticityDoubleLayerWrapper") + + self.base_kernel = base_kernel + + self.kernel_dict = {} + + 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) + + # 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): + 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] + + # For elasticity (nu != 0.5), we need the LaplaceKernel + if nu_sym != 0.5: + self.kernel_dict["laplace"] = LaplaceKernel(self.dim) + + 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. + """ + + 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)/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 + + result = 0 + for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ + zip(kernel_indices, dir_vec_indices, coeffs, + extra_deriv_dirs_vec): + if coeff == 0: + continue + 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 + return result/(2*(1 - nu)) + + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=()): + + 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=extra_deriv_dirs) + + return np.array(rewrite_using_base_kernel(sym_expr, + base_kernel=self.base_kernel)) + + def apply_single_and_double_layer(self, stokeslet_density_vec_sym, + stresslet_density_vec_sym, dir_vec_sym, + qbx_forced_limit, stokeslet_weight, stresslet_weight, + extra_deriv_dirs=()): + + stokeslet_obj = _ElasticityWrapperNaiveOrBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) + + 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, + qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight + + return sym_expr + + +class ElasticityDoubleLayerWrapperNaive( + _ElasticityDoubleLayerWrapperNaiveOrBiharmonic): + def __init__(self, dim, mu_sym, nu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + base_kernel=None) + + +class ElasticityDoubleLayerWrapperBiharmonic( + _ElasticityDoubleLayerWrapperNaiveOrBiharmonic): + def __init__(self, dim, mu_sym, nu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + base_kernel=BiharmonicKernel(dim)) + +# }}} + + +# {{{ dispatch function + +def create_elasticity_wrapper(dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT, + method="naive"): + + if nu_sym == 0.5: + from pytential.symbolic.stokes import StokesletWrapper + return StokesletWrapper(dim=dim, mu_sym=mu_sym, method=method) + if method == "naive": + return ElasticityWrapperNaive(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "biharmonic": + return ElasticityWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + elif method == "laplace": + if nu_sym == 0.5: + from pytential.symbolic.stokes import StokesletWrapperTornberg + return StokesletWrapperTornberg(dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) + else: + return ElasticityWrapperYoshida(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") + + +def create_elasticity_double_layer_wrapper(dim, mu_sym=_MU_SYM_DEFAULT, + nu_sym=_NU_SYM_DEFAULT, method="naive"): + + if nu_sym == 0.5: + from pytential.symbolic.stokes import StressletWrapper + return StressletWrapper(dim=dim, mu_sym=mu_sym, method=method) + if method == "naive": + return ElasticityDoubleLayerWrapperNaive(dim=dim, mu_sym=mu_sym, + nu_sym=nu_sym) + elif method == "biharmonic": + return ElasticityDoubleLayerWrapperBiharmonic(dim=dim, mu_sym=mu_sym, + nu_sym=nu_sym) + elif method == "laplace": + if nu_sym == 0.5: + from pytential.symbolic.stokes import StressletWrapperTornberg + return StressletWrapperTornberg(dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) + else: + return ElasticityDoubleLayerWrapperYoshida(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") + + +# }}} + + +# {{{ Yoshida + +class ElasticityDoubleLayerWrapperYoshida(ElasticityDoubleLayerWrapperBase): + """ElasticityDoubleLayer 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 @@ -48,18 +442,18 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " - "StressletWrapperYoshida") + "ElasticityDoubleLayerWrapperYoshida") 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=()): - return self.apply_stokeslet_and_stresslet([0]*self.dim, + return self.apply_single_and_double_layer([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, + def apply_single_and_double_layer(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, extra_deriv_dirs=()): @@ -145,8 +539,8 @@ def Q(i, int_g): return sym_expr -class StokesletWrapperYoshida(StokesletWrapperBase): - """Stokeslet Wrapper using Yoshida et al's method [1] which uses Laplace +class ElasticityWrapperYoshida(ElasticityWrapperBase): + """Elasticity single layer using Yoshida et al's method [1] which uses Laplace derivatives. [1] Yoshida, K. I., Nishimura, N., & Kobayashi, S. (2001). Application of @@ -160,13 +554,15 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " - "StokesletWrapperYoshida") + "ElasticityWrapperYoshida") self.kernel = LaplaceKernel(dim=3) self.mu = mu_sym self.nu = nu_sym - self.stresslet = StressletWrapperYoshida(3, self.mu, self.nu) + self.stresslet = ElasticityDoubleLayerWrapperYoshida(3, self.mu, self.nu) def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): - return self.stresslet.apply_stokeslet_and_stresslet(density_vec_sym, + return self.stresslet.apply_single_and_double_layer(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 e26ebf6c8..75a33210d 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,15 +27,20 @@ from pytential import sym from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel -from sumpy.kernel import (StressletKernel, LaplaceKernel, - ElasticityKernel, BiharmonicKernel, +from sumpy.kernel import (LaplaceKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) -from sumpy.symbolic import SpatialConstant -from abc import ABC, abstractmethod +from pytential.symbolic.elasticity import (ElasticityWrapperBase, + ElasticityDoubleLayerWrapperBase, + ElasticityWrapperNaive, ElasticityDoubleLayerWrapperNaive, + ElasticityWrapperBiharmonic, ElasticityDoubleLayerWrapperBiharmonic, + _MU_SYM_DEFAULT) +from abc import abstractmethod __doc__ = """ -.. autoclass:: StokesletWrapper -.. autoclass:: StressletWrapper +.. autoclass:: StokesletWrapperBase +.. autoclass:: StressletWrapperBase +.. automethod:: StokesletWrapper +.. automethod:: StressletWrapper .. autoclass:: StokesOperator .. autoclass:: HsiaoKressExteriorStokesOperator @@ -43,58 +48,23 @@ """ -# {{{ StokesletWrapper/StressletWrapper ABCs +# {{{ StokesletWrapper/StressletWrapper base classes -_MU_SYM_DEFAULT = SpatialConstant("mu") - - -class StokesletWrapperBase(ABC): +class StokesletWrapperBase(ElasticityWrapperBase): """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). + In addition to the methods in + :class:`pytential.symbolic.elasticity.ElasticityWrapperBase`, this class + also provides :meth:`apply_stress` which applies symmetric viscous stress tensor + in the requested direction and :meth:`apply_pressure`. .. automethod:: apply .. automethod:: apply_pressure .. automethod:: apply_derivative .. automethod:: apply_stress """ - def __init__(self, dim, mu_sym, nu_sym): - self.dim = dim - self.mu = mu_sym - self.nu = nu_sym - - @abstractmethod - 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 - 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 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 + def __init__(self, dim, mu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5) def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """Symbolic expression for pressure field associated with the Stokeslet.""" @@ -112,20 +82,6 @@ def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()) qbx_forced_limit=qbx_forced_limit) return 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, dir_vec_sym, qbx_forced_limit): r"""Symbolic expression for viscous stress applied to a direction. @@ -152,54 +108,21 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): raise NotImplementedError -class StressletWrapperBase(ABC): +class StressletWrapperBase(ElasticityDoubleLayerWrapperBase): """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). + In addition to the methods in + :class:`pytential.symbolic.elasticity.ElasticityDoubleLayerWrapperBase`, this + class also provides :meth:`apply_stress` which applies symmetric viscous stress + tensor in the requested direction and :meth:`apply_pressure`. .. automethod:: apply .. automethod:: apply_pressure .. automethod:: apply_derivative .. automethod:: apply_stress """ - def __init__(self, dim, mu_sym, nu_sym): - self.dim = dim - self.mu = mu_sym - self.nu = nu_sym - - @abstractmethod - 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 - 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 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 + def __init__(self, dim, mu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5) def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -226,23 +149,6 @@ def apply_pressure(self, density_vec_sym, dir_vec_sym, 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 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 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, dir_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. @@ -268,75 +174,9 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # }}} -# {{{ StokesletWrapper Naive and Biharmonic impl - -def _create_int_g(knl, deriv_dirs, density, **kwargs): - for deriv_dir in deriv_dirs: - knl = AxisTargetDerivative(deriv_dir, knl) - - 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) - - res = sym.int_g_vec(knl, density, **kwargs) - return res - +# {{{ Stokeslet/StressletWrapper Naive and Biharmonic class _StokesletWrapperNaiveOrBiharmonic(StokesletWrapperBase): - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, - base_kernel=None): - super().__init__(dim, mu_sym, nu_sym) - if not (dim == 3 or dim == 2): - raise ValueError("unsupported dimension given to StokesletWrapper") - - self.base_kernel = base_kernel - - 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 - - # 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, dim): - self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, - jcomp=j, poisson_ratio=poisson_ratio) - self.kernel_dict[(j, i)] = self.kernel_dict[(i, j)] - - 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. - """ - res = _create_int_g(self.kernel_dict[idx], deriv_dirs, - density=density_sym*dir_vec_sym[idx[-1]], - 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=()): - - 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], [1]*self.dim, - qbx_forced_limit, deriv_dirs=extra_deriv_dirs) - - return np.array(rewrite_using_base_kernel(sym_expr, - base_kernel=self.base_kernel)) - def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): sym_expr = super().apply_pressure(density_vec_sym, qbx_forced_limit, @@ -344,179 +184,34 @@ def apply_pressure(self, density_vec_sym, qbx_forced_limit, res, = rewrite_using_base_kernel([sym_expr], base_kernel=self.base_kernel) return res - def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): - - sym_expr = np.zeros((self.dim,), dtype=object) - stresslet_obj = _StressletWrapperNaiveOrBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) - - # 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], [1]*self.dim, - qbx_forced_limit, deriv_dirs=[]) - - return sym_expr - - -class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic): - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - base_kernel=None) - - -class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic): - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - base_kernel=BiharmonicKernel(dim)) - - -# }}} - - -# {{{ StressletWrapper Naive and Biharmonic impl class _StressletWrapperNaiveOrBiharmonic(StressletWrapperBase): - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, - base_kernel=None): - super().__init__(dim, mu_sym, nu_sym) - if not (dim == 3 or dim == 2): - raise ValueError("unsupported dimension given to StokesletWrapper") - - self.base_kernel = base_kernel - - self.kernel_dict = {} - - 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) - - # 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): - 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] - - # For elasticity (nu != 0.5), we need the LaplaceKernel - if nu_sym != 0.5: - self.kernel_dict["laplace"] = LaplaceKernel(self.dim) - - 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. - """ - - 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)/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 - - result = 0 - for kernel_idx, dir_vec_idx, coeff, extra_deriv_dirs in \ - zip(kernel_indices, dir_vec_indices, coeffs, - extra_deriv_dirs_vec): - if coeff == 0: - continue - 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 - return result/(2*(1 - nu)) - - def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, - extra_deriv_dirs=()): - - 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=extra_deriv_dirs) - - 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, - qbx_forced_limit, stokeslet_weight, stresslet_weight, + def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): + sym_expr = super().apply_pressure(density_vec_sym, qbx_forced_limit, + extra_deriv_dirs=extra_deriv_dirs) + res, = rewrite_using_base_kernel([sym_expr], base_kernel=self.base_kernel) + return res - stokeslet_obj = _StokesletWrapperNaiveOrBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) - - 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, - qbx_forced_limit, extra_deriv_dirs) * stokeslet_weight - - return sym_expr - - def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, - qbx_forced_limit): - - sym_expr = np.empty((self.dim,), dtype=object) - - # Build velocity derivative matrix - 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, 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, - qbx_forced_limit) +class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic, + ElasticityWrapperNaive): + pass - # Now add the velocity derivative components - for j in range(self.dim): - sym_expr[comp] = sym_expr[comp] + ( - dir_vec_sym[j] * self.mu * ( - sym_grad_matrix[comp][j] - + sym_grad_matrix[j][comp]) - ) - return sym_expr +class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic, + ElasticityDoubleLayerWrapperNaive): + pass -class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic): - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - base_kernel=None) +class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic, + ElasticityWrapperBiharmonic): + pass -class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic): - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, - base_kernel=BiharmonicKernel(dim)) +class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic, + ElasticityDoubleLayerWrapperBiharmonic): + pass # }}} @@ -542,7 +237,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): stresslet = StressletWrapperTornberg(self.dim, self.mu, self.nu) - return stresslet.apply_stokeslet_and_stresslet(density_vec_sym, + return stresslet.apply_single_and_double_layer(density_vec_sym, [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) @@ -565,10 +260,10 @@ def __init__(self, dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): - return self.apply_stokeslet_and_stresslet([0]*self.dim, + return self.apply_single_and_double_layer([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, + def apply_single_and_double_layer(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, qbx_forced_limit, stokeslet_weight, stresslet_weight, extra_deriv_dirs=()): @@ -634,49 +329,37 @@ def apply_stokeslet_and_stresslet(self, stokeslet_density_vec_sym, # }}} -# {{{ StokesletWrapper dispatch class +# {{{ StokesletWrapper dispatch method -def StokesletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): +def StokesletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, method=None): # noqa: N806 if method is 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) + return StokesletWrapperNaive(dim=dim, mu_sym=mu_sym) elif method == "biharmonic": - return StokesletWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + return StokesletWrapperBiharmonic(dim=dim, mu_sym=mu_sym) elif method == "laplace": - 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) + return StokesletWrapperTornberg(dim=dim, mu_sym=mu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") -def StressletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5, method=None): +def StressletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, method=None): # noqa: N806 if method is 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) + return StressletWrapperNaive(dim=dim, mu_sym=mu_sym) elif method == "biharmonic": - return StressletWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + return StressletWrapperBiharmonic(dim=dim, mu_sym=mu_sym) elif method == "laplace": - 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) + return StressletWrapperTornberg(dim=dim, mu_sym=mu_sym) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -700,7 +383,7 @@ class StokesOperator: .. automethod:: pressure """ - def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): + def __init__(self, ambient_dim, side, stokeslet, stresslet, mu_sym): """ :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. @@ -710,13 +393,24 @@ def __init__(self, ambient_dim, side, method, mu_sym, nu_sym): self.ambient_dim = ambient_dim self.side = side - self.mu = mu_sym - self.nu = nu_sym - self.stresslet = StressletWrapper(dim=self.ambient_dim, - 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, method=method) + if mu_sym is not None: + import warnings + warnings.warn("Explicitly giving mu_sym is deprecated. " + "Use stokeslet and stresslet arguments.") + else: + mu_sym = _MU_SYM_DEFAULT + + if stresslet is None: + stresslet = StressletWrapper(dim=self.ambient_dim, + mu_sym=mu_sym) + + if stokeslet is None: + stokeslet = StokesletWrapper(dim=self.ambient_dim, + mu_sym=mu_sym) + + self.stokeslet = stokeslet + self.stresslet = stresslet @property def dim(self): @@ -775,8 +469,8 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, omega, alpha=1.0, eta=1.0, method="biharmonic", - mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + def __init__(self, *, omega, alpha=1.0, eta=1.0, + stokeslet=None, stresslet=None, mu_sym=None): r""" :arg omega: farfield behaviour of the velocity field, as defined by :math:`A` in [HsiaoKress1985]_ Equation 2.3. @@ -784,8 +478,8 @@ def __init__(self, *, omega, alpha=1.0, eta=1.0, method="biharmonic", :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, - mu_sym=mu_sym, nu_sym=nu_sym) + super().__init__(ambient_dim=2, side=+1, stokeslet=stokeslet, + stresslet=stresslet, mu_sym=mu_sym) # NOTE: in [hsiao-kress], there is an analysis on a circle, which # recommends values in @@ -800,7 +494,7 @@ 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) - result = self.stresslet.apply_stokeslet_and_stresslet( + result = self.stresslet.apply_single_and_double_layer( -self.omega / length, [0]*self.ambient_dim, [0]*self.ambient_dim, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=1, stresslet_weight=0) @@ -817,7 +511,7 @@ def _operator(self, sigma, normal, qbx_forced_limit): self.dim, sigma, dofdesc=dd)) result = self.eta * self.alpha / (2.0 * np.pi) * int_sigma - result += self.stresslet.apply_stokeslet_and_stresslet(meanless_sigma, + result += self.stresslet.apply_single_and_double_layer(meanless_sigma, sigma, normal, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=-self.eta, stresslet_weight=1) @@ -854,15 +548,14 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None, method="laplace", mu_sym=_MU_SYM_DEFAULT, - nu_sym=0.5): + def __init__(self, *, eta=None, stokeslet=None, stresslet=None, mu_sym=None): 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, - mu_sym=mu_sym, nu_sym=nu_sym) + super().__init__(ambient_dim=3, side=+1, stokeslet=stokeslet, + stresslet=stresslet, mu_sym=mu_sym) # NOTE: eta is chosen here based on H. 1986 Figure 1, which is # based on solving on the unit sphere @@ -873,7 +566,7 @@ 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): - result = self.stresslet.apply_stokeslet_and_stresslet(sigma, + result = self.stresslet.apply_single_and_double_layer(sigma, sigma, normal, qbx_forced_limit=qbx_forced_limit, stokeslet_weight=self.eta, stresslet_weight=1, extra_deriv_dirs=()) diff --git a/test/test_stokes.py b/test/test_stokes.py index 88dc3e60d..59bc9a01a 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -27,8 +27,10 @@ from arraycontext import flatten from pytential import GeometryCollection, bind, sym -from pytential.symbolic.stokes import (StokesletWrapper, StressletWrapper, - StressletWrapperBase) +from pytential.symbolic.stokes import StokesletWrapper +from pytential.symbolic.elasticity import (create_elasticity_wrapper, + create_elasticity_double_layer_wrapper, + ElasticityDoubleLayerWrapperBase) from meshmode.discretization import Discretization from meshmode.discretization.poly_element import \ InterpolatoryQuadratureGroupFactory @@ -163,15 +165,19 @@ def run_exterior_stokes(actx_factory, *, else: sym_nu = SpatialConstant("nu2") + stokeslet = create_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, + nu_sym=sym_nu, method=method) + stresslet = create_elasticity_double_layer_wrapper(ambient_dim, mu_sym=sym_mu, + nu_sym=sym_nu, method=method) + if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator sym_omega = sym.make_sym_vector("omega", ambient_dim) - op = HsiaoKressExteriorStokesOperator(omega=sym_omega, method=method, - mu_sym=sym_mu, nu_sym=sym_nu) + op = HsiaoKressExteriorStokesOperator(omega=sym_omega, stokeslet=stokeslet, + stresslet=stresslet) elif ambient_dim == 3: from pytential.symbolic.stokes import HebekerExteriorStokesOperator - op = HebekerExteriorStokesOperator(method=method, - mu_sym=sym_mu, nu_sym=sym_nu) + op = HebekerExteriorStokesOperator(stokeslet=stokeslet, stresslet=stresslet) else: raise AssertionError() @@ -188,7 +194,7 @@ def run_exterior_stokes(actx_factory, *, else: # Use the naive method here as biharmonic requires source derivatives # of point_source - sym_source_pot = StokesletWrapper(ambient_dim, mu_sym=sym_mu, + sym_source_pot = create_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -583,7 +589,7 @@ def apply_operator(self): "density_vec_sym": [1]*dim, "qbx_forced_limit": 1, } - if isinstance(self.operator, StressletWrapperBase): + if isinstance(self.operator, ElasticityDoubleLayerWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() d_u = [self.operator.apply(**args, extra_deriv_dirs=(i,)) @@ -611,7 +617,7 @@ def apply_operator(self): "density_vec_sym": [1]*dim, "qbx_forced_limit": 1, } - if isinstance(self.operator, StressletWrapperBase): + if isinstance(self.operator, ElasticityDoubleLayerWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() mu = self.operator.mu @@ -676,8 +682,8 @@ def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): else: pde_class = ElasticityPDE - identity = pde_class(dim, - StokesletWrapper(case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) + identity = pde_class(dim, create_elasticity_wrapper( + case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) for resolution in resolutions: h_max, errors = run_stokes_identity( @@ -723,8 +729,8 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): else: pde_class = ElasticityPDE - identity = pde_class(dim, - StressletWrapper(case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) + identity = pde_class(dim, create_elasticity_double_layer_wrapper( + case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) from pytools.convergence import EOCRecorder eocs = [EOCRecorder() for _ in range(case.ambient_dim)] From 911f42ba77f0cde674bb24023753d0cacfd6b1e9 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 02:54:41 -0500 Subject: [PATCH 050/156] restore apply_pressure and apply_stress --- pytential/symbolic/stokes.py | 51 +++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 75a33210d..2a4bbe3fa 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -184,15 +184,60 @@ def apply_pressure(self, density_vec_sym, qbx_forced_limit, res, = rewrite_using_base_kernel([sym_expr], base_kernel=self.base_kernel) return res + def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): + + sym_expr = np.zeros((self.dim,), dtype=object) + stresslet_obj = _StressletWrapperNaiveOrBiharmonic(dim=self.dim, + mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) + + # 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], [1]*self.dim, + qbx_forced_limit, deriv_dirs=[]) + + return sym_expr + class _StressletWrapperNaiveOrBiharmonic(StressletWrapperBase): - def apply_pressure(self, density_vec_sym, qbx_forced_limit, + def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): - sym_expr = super().apply_pressure(density_vec_sym, qbx_forced_limit, - extra_deriv_dirs=extra_deriv_dirs) + sym_expr = super().apply_pressure(density_vec_sym, dir_vec_sym, + qbx_forced_limit, extra_deriv_dirs=extra_deriv_dirs) res, = rewrite_using_base_kernel([sym_expr], base_kernel=self.base_kernel) return res + def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, + qbx_forced_limit): + + sym_expr = np.empty((self.dim,), dtype=object) + + # Build velocity derivative matrix + 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, 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, + 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] * self.mu * ( + sym_grad_matrix[comp][j] + + sym_grad_matrix[j][comp]) + ) + return sym_expr + class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic, ElasticityWrapperNaive): From f05e277fa73d02a98855c278d75810cba6730497 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 15:43:11 -0500 Subject: [PATCH 051/156] Fix inheritance --- pytential/symbolic/elasticity.py | 37 ++++++++++++++++++-------- pytential/symbolic/stokes.py | 45 ++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 7fd3f92da..370592042 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -34,8 +34,8 @@ from abc import ABC, abstractmethod __doc__ = """ -.. autoclass:: ElasticityWrapper -.. autoclass:: ElasticityDoubleLayerWrapper +.. autoclass:: ElasticityWrapperBase +.. autoclass:: ElasticityDoubleLayerWrapperBase """ @@ -189,9 +189,12 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): return res -class _ElasticityWrapperNaiveOrBiharmonic(ElasticityWrapperBase): +class _ElasticityWrapperNaiveOrBiharmonic: def __init__(self, dim, mu_sym, nu_sym, base_kernel): - super().__init__(dim, mu_sym, nu_sym) + self.dim = dim + self.mu = mu_sym + self.nu = nu_sym + if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to ElasticityWrapper") @@ -239,15 +242,19 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): base_kernel=self.base_kernel)) -class ElasticityWrapperNaive(_ElasticityWrapperNaiveOrBiharmonic): +class ElasticityWrapperNaive(_ElasticityWrapperNaiveOrBiharmonic, + ElasticityWrapperBase): def __init__(self, dim, mu_sym, nu_sym): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, base_kernel=None) + ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) -class ElasticityWrapperBiharmonic(_ElasticityWrapperNaiveOrBiharmonic): +class ElasticityWrapperBiharmonic(_ElasticityWrapperNaiveOrBiharmonic, + ElasticityWrapperBase): def __init__(self, dim, mu_sym, nu_sym): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, base_kernel=BiharmonicKernel(dim)) + ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) # }}} @@ -255,11 +262,13 @@ def __init__(self, dim, mu_sym, nu_sym): # {{{ ElasticityDoubleLayerWrapper Naive and Biharmonic impl -class _ElasticityDoubleLayerWrapperNaiveOrBiharmonic( - ElasticityDoubleLayerWrapperBase): +class _ElasticityDoubleLayerWrapperNaiveOrBiharmonic: def __init__(self, dim, mu_sym, nu_sym, base_kernel): - super().__init__(dim, mu_sym, nu_sym) + self.dim = dim + self.mu = mu_sym + self.nu = nu_sym + if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to " "ElasticityDoubleLayerWrapper") @@ -357,17 +366,23 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, class ElasticityDoubleLayerWrapperNaive( - _ElasticityDoubleLayerWrapperNaiveOrBiharmonic): + _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, + ElasticityDoubleLayerWrapperBase): def __init__(self, dim, mu_sym, nu_sym): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, base_kernel=None) + ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) class ElasticityDoubleLayerWrapperBiharmonic( - _ElasticityDoubleLayerWrapperNaiveOrBiharmonic): + _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, + ElasticityDoubleLayerWrapperBase): def __init__(self, dim, mu_sym, nu_sym): super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, base_kernel=BiharmonicKernel(dim)) + ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, + mu_sym=mu_sym, nu_sym=nu_sym) # }}} diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 2a4bbe3fa..c3634386b 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -27,12 +27,12 @@ from pytential import sym from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel -from sumpy.kernel import (LaplaceKernel, +from sumpy.kernel import (LaplaceKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from pytential.symbolic.elasticity import (ElasticityWrapperBase, ElasticityDoubleLayerWrapperBase, - ElasticityWrapperNaive, ElasticityDoubleLayerWrapperNaive, - ElasticityWrapperBiharmonic, ElasticityDoubleLayerWrapperBiharmonic, + _ElasticityWrapperNaiveOrBiharmonic, + _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, _MU_SYM_DEFAULT) from abc import abstractmethod @@ -176,7 +176,9 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, # {{{ Stokeslet/StressletWrapper Naive and Biharmonic -class _StokesletWrapperNaiveOrBiharmonic(StokesletWrapperBase): +class _StokesletWrapperNaiveOrBiharmonic(_ElasticityWrapperNaiveOrBiharmonic, + StokesletWrapperBase): + def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): sym_expr = super().apply_pressure(density_vec_sym, qbx_forced_limit, @@ -188,7 +190,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = _StressletWrapperNaiveOrBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) + mu_sym=self.mu, nu_sym=0.5, base_kernel=self.base_kernel) # For stokeslet, there's no direction vector involved # passing a list of ones instead to remove its usage. @@ -203,7 +205,9 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): return sym_expr -class _StressletWrapperNaiveOrBiharmonic(StressletWrapperBase): +class _StressletWrapperNaiveOrBiharmonic( + _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, + StressletWrapperBase): def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): sym_expr = super().apply_pressure(density_vec_sym, dir_vec_sym, @@ -240,23 +244,36 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic, - ElasticityWrapperNaive): - pass + ElasticityWrapperBase): + def __init__(self, dim, mu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, base_kernel=None) + ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=0.5) class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic, - ElasticityDoubleLayerWrapperNaive): - pass + ElasticityDoubleLayerWrapperBase): + + def __init__(self, dim, mu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, base_kernel=None) + ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, + nu_sym=0.5) class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic, - ElasticityWrapperBiharmonic): - pass + ElasticityWrapperBase): + def __init__(self, dim, mu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, + base_kernel=BiharmonicKernel(dim)) + ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=0.5) class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic, - ElasticityDoubleLayerWrapperBiharmonic): - pass + ElasticityDoubleLayerWrapperBase): + def __init__(self, dim, mu_sym): + super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, + base_kernel=BiharmonicKernel(dim)) + ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, + nu_sym=0.5) # }}} From 14097661532173d4861631cd23e61cc83c783f06 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 15:44:54 -0500 Subject: [PATCH 052/156] Fix docs --- 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 c3634386b..432efdf33 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -96,7 +96,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): 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 + :class:`StressletWrapperBase`. 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. From 879b2788198918abf0b639fcfe82335d9a9f2ab4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 16:24:37 -0500 Subject: [PATCH 053/156] More doc fixes --- doc/symbolic.rst | 10 ++++++++ pytential/symbolic/elasticity.py | 39 ++++++++++++++++++++++++++++---- pytential/symbolic/stokes.py | 5 ++-- pytential/symbolic/typing.py | 6 ++--- 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/doc/symbolic.rst b/doc/symbolic.rst index d2a3bf8c7..7a4c068b5 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -33,6 +33,11 @@ Maxwell's equations .. automodule:: pytential.symbolic.pde.maxwell +Elasticity equations +^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: pytential.symbolic.elasticity + Stokes' equations ^^^^^^^^^^^^^^^^^ @@ -57,3 +62,8 @@ How a symbolic operator gets executed .. automodule:: pytential.symbolic.execution .. automodule:: pytential.symbolic.compiler + +Rewriting expressions with IntGs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: pytential.symbolic.pde.system_utils diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 370592042..0094fe079 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -32,10 +32,14 @@ AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant from abc import ABC, abstractmethod +from pytential.symbolic.typing import ExpressionT __doc__ = """ .. autoclass:: ElasticityWrapperBase .. autoclass:: ElasticityDoubleLayerWrapperBase + +.. automethod:: pytential.symbolic.elasticity.create_elasticity_wrapper +.. automethod:: pytential.symbolic.elasticity.create_elasticity_double_layer_wrapper """ @@ -389,8 +393,22 @@ def __init__(self, dim, mu_sym, nu_sym): # {{{ dispatch function -def create_elasticity_wrapper(dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT, - method="naive"): +def create_elasticity_wrapper( + dim: int, + mu_sym: ExpressionT = _MU_SYM_DEFAULT, + nu_sym: ExpressionT = _NU_SYM_DEFAULT, + method:str = "naive") -> ElasticityWrapperBase: + """Creates a :class:`ElasticityWrapperBase` object depending on the input + values. + + :param: dim: dimension + :param: mu_sym: viscosity symbol, defaults to "mu" + :param: nu_sym: poisson ratio symbol, defaults to "nu" + :param: method: method to use, defaults to "naive". + One of ("naive", "laplace", "biharmonic") + + :return: a :class:`ElasticityWrapperBase` object + """ if nu_sym == 0.5: from pytential.symbolic.stokes import StokesletWrapper @@ -412,9 +430,22 @@ def create_elasticity_wrapper(dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAUL "Needs to be one of naive, laplace, biharmonic") -def create_elasticity_double_layer_wrapper(dim, mu_sym=_MU_SYM_DEFAULT, - nu_sym=_NU_SYM_DEFAULT, method="naive"): +def create_elasticity_double_layer_wrapper( + dim: int, + mu_sym: ExpressionT = _MU_SYM_DEFAULT, + nu_sym: ExpressionT = _NU_SYM_DEFAULT, + method:str = "naive") -> ElasticityDoubleLayerWrapperBase: + """Creates a :class:`ElasticityDoubleLayerWrapperBase` object depending on the + input values. + :param: dim: dimension + :param: mu_sym: viscosity symbol, defaults to "mu" + :param: nu_sym: poisson ratio symbol, defaults to "nu" + :param: method: method to use, defaults to "naive". + One of ("naive", "laplace", "biharmonic") + + :return: a :class:`ElasticityDoubleLayerWrapperBase` object + """ if nu_sym == 0.5: from pytential.symbolic.stokes import StressletWrapper return StressletWrapper(dim=dim, mu_sym=mu_sym, method=method) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 432efdf33..48e41d653 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -37,10 +37,11 @@ from abc import abstractmethod __doc__ = """ + .. autoclass:: StokesletWrapperBase .. autoclass:: StressletWrapperBase -.. automethod:: StokesletWrapper -.. automethod:: StressletWrapper +.. automethod:: pytential.symbolic.stokes.StokesletWrapper +.. automethod:: pytential.symbolic.stokes.StressletWrapper .. autoclass:: StokesOperator .. autoclass:: HsiaoKressExteriorStokesOperator diff --git a/pytential/symbolic/typing.py b/pytential/symbolic/typing.py index 7a5fac687..e0e23d063 100644 --- a/pytential/symbolic/typing.py +++ b/pytential/symbolic/typing.py @@ -23,9 +23,7 @@ import numpy as np from pymbolic.primitives import Expression -IntegralT = Union[int, np.int8, np.int16, np.int32, np.int64, np.uint8, - np.uint16, np.uint32, np.uint64] -FloatT = Union[float, complex, np.float32, np.float64, np.complex64, - np.complex128] +IntegralT = Union[int, np.integer] +FloatT = Union[float, complex, np.floating, np.complexfloating] ExpressionT = Union[IntegralT, FloatT, Expression] From 7b92763cbbfa011ebf856a0d118a6637ae1f2ea0 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 16:33:23 -0500 Subject: [PATCH 054/156] doc internals --- doc/symbolic.rst | 13 ++++++++++--- pytential/symbolic/pde/system_utils.py | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/doc/symbolic.rst b/doc/symbolic.rst index 7a4c068b5..1e667a6a2 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -48,6 +48,11 @@ Scalar Beltrami equations .. automodule:: pytential.symbolic.pde.beltrami +Rewriting expressions with IntGs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: pytential.symbolic.pde.system_utils + Internal affairs ---------------- @@ -63,7 +68,9 @@ How a symbolic operator gets executed .. automodule:: pytential.symbolic.compiler -Rewriting expressions with IntGs -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Rewriting expressions with IntGs internals +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automethod:: pytential.symbolic.pde.system_utils.convert_target_transformation_to_source +.. automethod:: pytential.symbolic.pde.system_utils.convert_int_g_to_base -.. automodule:: pytential.symbolic.pde.system_utils diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 08c660f14..eca20d5e1 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -139,8 +139,8 @@ def _monom_to_expr(monom: List[int], def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: """Convert an ``IntG`` with :class:`sumpy.kernel.AxisTargetDerivative` - or :class:`sumpy.kernel.TargetMultiplier` to a list - of ``IntG``s without them and only source dependent transformations. + or :class:`sumpy.kernel.TargetPointMultiplier` to a list + of ``IntG``\s without them and only source dependent transformations. The sum of the list returned is equivalent to the input *int_g*. For example:: From affd89d49d025942acd7729a2080c7d0a758eab7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 16:37:36 -0500 Subject: [PATCH 055/156] Fix formatting --- pytential/symbolic/elasticity.py | 4 ++-- pytential/symbolic/pde/system_utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 0094fe079..a7ffd16df 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -397,7 +397,7 @@ def create_elasticity_wrapper( dim: int, mu_sym: ExpressionT = _MU_SYM_DEFAULT, nu_sym: ExpressionT = _NU_SYM_DEFAULT, - method:str = "naive") -> ElasticityWrapperBase: + method: str = "naive") -> ElasticityWrapperBase: """Creates a :class:`ElasticityWrapperBase` object depending on the input values. @@ -434,7 +434,7 @@ def create_elasticity_double_layer_wrapper( dim: int, mu_sym: ExpressionT = _MU_SYM_DEFAULT, nu_sym: ExpressionT = _NU_SYM_DEFAULT, - method:str = "naive") -> ElasticityDoubleLayerWrapperBase: + method: str = "naive") -> ElasticityDoubleLayerWrapperBase: """Creates a :class:`ElasticityDoubleLayerWrapperBase` object depending on the input values. diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index eca20d5e1..582f7b321 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -140,7 +140,7 @@ def _monom_to_expr(monom: List[int], def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: """Convert an ``IntG`` with :class:`sumpy.kernel.AxisTargetDerivative` or :class:`sumpy.kernel.TargetPointMultiplier` to a list - of ``IntG``\s without them and only source dependent transformations. + of ``IntG`` s without them and only source dependent transformations. The sum of the list returned is equivalent to the input *int_g*. For example:: From a74f619782d0122b4df5114c5370b53fb3391c64 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 17:03:00 -0500 Subject: [PATCH 056/156] lu_solve_with_expand -> lu_with_post_division_callback --- pytential/symbolic/pde/system_utils.py | 4 ++-- pytential/utils.py | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 582f7b321..1dde70937 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -34,7 +34,7 @@ from pytential.symbolic.primitives import (NodeCoordinateComponent, hashable_kernel_args, IntG) from pytential.symbolic.mappers import IdentityMapper -from pytential.utils import chop, lu_solve_with_expand +from pytential.utils import chop, lu_with_post_division_callback import pytential from typing import List, Mapping, Text, Any, Union, Tuple, Optional @@ -387,7 +387,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, const = 0 logger.debug("%s = ", kernel) - sol = lu_solve_with_expand(L, U, perm, vec) + sol = lu_with_post_division_callback(L, U, perm, vec, lambda expr: expr.expand()) for i, coeff in enumerate(sol): coeff = chop(coeff, tol) if coeff == 0: diff --git a/pytential/utils.py b/pytential/utils.py index 68446b3c7..8286cf8d5 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -54,7 +54,7 @@ def chop(expr, tol): return expr.xreplace(replace_dict) -def lu_solve_with_expand(L, U, perm, b): +def lu_with_post_division_callback(L, U, perm, b, callback): """Given an LU factorization and a vector, solve a linear system with intermediate results expanded to avoid an explosion of the expression trees @@ -63,6 +63,7 @@ def lu_solve_with_expand(L, U, perm, b): :param U: upper triangular matrix :param perm: permutation matrix :param b: column vector to solve for + :param callback: callable that is called after each division """ def forward_substitution(L, b): n = len(b) @@ -70,7 +71,7 @@ def forward_substitution(L, 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() + res[i] = callback(res[i] / L[i, i]) return res def backward_substitution(U, b): @@ -79,7 +80,7 @@ def backward_substitution(U, 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() + res[i] = callback(res[i] / U[i, i]) return res def permute_fwd(b, perm): From 09c09d5cfd1386b07b80f7b41140b0866ef73eb2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Sep 2022 17:17:50 -0500 Subject: [PATCH 057/156] Fix default source/target If DEFAULT_SOURCE/TARGET is used, the location tagger will use the default for the tagger. --- pytential/symbolic/pde/system_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 1dde70937..869fb56c9 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -32,7 +32,7 @@ as gnitstam) from pytential.symbolic.primitives import (NodeCoordinateComponent, - hashable_kernel_args, IntG) + hashable_kernel_args, IntG, as_dofdesc, DEFAULT_SOURCE) from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_with_post_division_callback import pytential @@ -305,8 +305,10 @@ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ # 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) + if int_g.source is None: + dd = as_dofdesc(DEFAULT_SOURCE) + else: + dd = int_g.source const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) if const != 0 and target_kernel != target_kernel.get_base_kernel(): From a6f23a0b65f1a0fc37eb2cc32d9770f810b39cfc Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 14 Sep 2022 02:15:30 -0500 Subject: [PATCH 058/156] Use a different source/target --- pytential/symbolic/dof_desc.py | 10 ++++++++++ pytential/symbolic/mappers.py | 4 ++-- pytential/symbolic/pde/system_utils.py | 4 ++-- pytential/symbolic/primitives.py | 1 + 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/dof_desc.py b/pytential/symbolic/dof_desc.py index 2660f3b1f..2abb7bc3a 100644 --- a/pytential/symbolic/dof_desc.py +++ b/pytential/symbolic/dof_desc.py @@ -77,6 +77,16 @@ class DEFAULT_TARGET: # noqa: N801 :func:`pytential.bind`.""" +class TAG_WITH_DEFAULT_SOURCE: # noqa: N801 + """Symbolic identifier for a source that is tagged with + the default source.""" + + +class TAG_WITH_DEFAULT_TARGET: # noqa: N801 + """Symbolic identifier for a source that is tagged with + the default target.""" + + class QBX_SOURCE_STAGE1: # noqa: N801 """Symbolic identifier for the Stage 1 discretization of a :class:`pytential.qbx.QBXLayerPotentialSource`. diff --git a/pytential/symbolic/mappers.py b/pytential/symbolic/mappers.py index e4081e0f8..5c29e5516 100644 --- a/pytential/symbolic/mappers.py +++ b/pytential/symbolic/mappers.py @@ -290,9 +290,9 @@ def _default_dofdesc(self, dofdesc): dofdesc = dofdesc.copy(geometry=self.default_target) else: dofdesc = dofdesc.copy(geometry=self.default_source) - elif dofdesc.geometry is prim.DEFAULT_SOURCE: + elif dofdesc.geometry is prim.TAG_WITH_DEFAULT_SOURCE: dofdesc = dofdesc.copy(geometry=self.default_source) - elif dofdesc.geometry is prim.DEFAULT_TARGET: + elif dofdesc.geometry is prim.TAG_WITH_DEFAULT_TARGET: dofdesc = dofdesc.copy(geometry=self.default_target) return dofdesc diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 869fb56c9..3c8912d04 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -32,7 +32,7 @@ as gnitstam) from pytential.symbolic.primitives import (NodeCoordinateComponent, - hashable_kernel_args, IntG, as_dofdesc, DEFAULT_SOURCE) + hashable_kernel_args, IntG, as_dofdesc, TAG_WITH_DEFAULT_SOURCE) from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_with_post_division_callback import pytential @@ -306,7 +306,7 @@ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ # on the source instead of the target when using automatic tagging # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` if int_g.source is None: - dd = as_dofdesc(DEFAULT_SOURCE) + dd = as_dofdesc(TAG_WITH_DEFAULT_SOURCE) else: dd = int_g.source const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 869a4e7f7..def661265 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -41,6 +41,7 @@ from pytential.symbolic.dof_desc import ( DEFAULT_SOURCE, DEFAULT_TARGET, + TAG_WITH_DEFAULT_SOURCE, TAG_WITH_DEFAULT_TARGET, QBX_SOURCE_STAGE1, QBX_SOURCE_STAGE2, QBX_SOURCE_QUAD_STAGE2, GRANULARITY_NODE, GRANULARITY_CENTER, GRANULARITY_ELEMENT, DOFDescriptor, DOFDescriptorLike, From e3f61d68e3cbad8293047b5a41a4cbc2c7d4a3d1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 01:36:40 -0600 Subject: [PATCH 059/156] Fix setting geometry --- 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 3c8912d04..1d1974768 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -305,8 +305,8 @@ def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ # 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` - if int_g.source is None: - dd = as_dofdesc(TAG_WITH_DEFAULT_SOURCE) + if int_g.source.geometry is None: + dd = int_g.source.copy(geometry=TAG_WITH_DEFAULT_SOURCE) else: dd = int_g.source const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) From db0d2ff50432143bc9ba97b73b4ad31ef546ecfb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 01:40:30 -0600 Subject: [PATCH 060/156] document the assumption that potentials are smooth --- 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 1d1974768..e2a72e70a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -63,7 +63,8 @@ def rewrite_using_base_kernel(exprs: List[ExpressionT], base_kernel=_NO_ARG_SENTINEL) -> List[ExpressionT]: """ Rewrites a list of expressions with :class:`~pytential.symbolic.primitives.IntG` - objects using *base_kernel*. + objects using *base_kernel*. Assumes that potentials are smooth, i.e. that + Schwarz's theorem holds. 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 From 623ed449c53a7cdd6939349450609eb705aa754c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 01:54:00 -0600 Subject: [PATCH 061/156] explain the weights --- pytential/symbolic/stokes.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 48e41d653..9eddc8e33 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -338,12 +338,18 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, k in range(self.dim)] common_source_kernels.append(self.kernel) - stresslet_weight *= 3.0/6 + # The paper in [1] ignores the scaling we use Stokeslet/Stresslet + # and gives formulae for the kernel expression only + # stokeslet_weight = StokesletKernel.global_scaling_const / + # LaplaceKernel.global_scaling_const + # stresslet_weight = StressletKernel.global_scaling_const / + # LaplaceKernel.global_scaling_const + stresslet_weight *= 3.0 stokeslet_weight *= -0.5*self.mu**(-1) for i in range(self.dim): for j in range(self.dim): - densities = [stresslet_weight*( + densities = [(stresslet_weight/6.0)*( stresslet_density_vec_sym[k] * dir_vec_sym[j] + stresslet_density_vec_sym[j] * dir_vec_sym[k]) for k in range(self.dim)] @@ -374,7 +380,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, k in range(self.dim)) 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] + densities = [(stresslet_weight/6.0)*(common_density0 * dir_vec_sym[k] + common_density1 * stresslet_density_vec_sym[k]) for k in range(self.dim)] densities.append(stokeslet_weight * common_density2) From 77a87af596031f3360515b71b7f0526549a3ce7f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 01:56:51 -0600 Subject: [PATCH 062/156] get_int_g -> _get_int_g --- pytential/symbolic/elasticity.py | 8 ++++---- pytential/symbolic/stokes.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index a7ffd16df..086fd8a08 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -218,7 +218,7 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): jcomp=j, poisson_ratio=poisson_ratio) self.kernel_dict[(j, i)] = self.kernel_dict[(i, j)] - def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, + def _get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): """ Returns the Integral of the elasticity kernel given by `idx` @@ -238,7 +238,7 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): # 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), + sym_expr[comp] += self._get_int_g((comp, i), density_vec_sym[i], [1]*self.dim, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) @@ -302,7 +302,7 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): if nu_sym != 0.5: self.kernel_dict["laplace"] = LaplaceKernel(self.dim) - def get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, + 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` @@ -343,7 +343,7 @@ def apply(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): - sym_expr[comp] += self.get_int_g((comp, i, j), + sym_expr[comp] += self._get_int_g((comp, i, j), density_vec_sym[i], dir_vec_sym, qbx_forced_limit, deriv_dirs=extra_deriv_dirs) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 9eddc8e33..cfeb7c11c 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -199,7 +199,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), + stresslet_obj._get_int_g((comp, i, j), density_vec_sym[j], [1]*self.dim, qbx_forced_limit, deriv_dirs=[]) From 6d505bc74a21ba322bf1b462d18e210a9cec5f84 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 01:58:15 -0600 Subject: [PATCH 063/156] simplify using sym.nodes --- 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 cfeb7c11c..df06f49f8 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -333,7 +333,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, sym_expr = np.zeros((self.dim,), dtype=object) - source = [sym.NodeCoordinateComponent(d) for d in range(self.dim)] + source = sym.nodes(self.dim).as_vector() common_source_kernels = [AxisSourceDerivative(k, self.kernel) for k in range(self.dim)] common_source_kernels.append(self.kernel) From f3fa8586595063b52cb5d8ff20bff8055672399b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 02:42:41 -0600 Subject: [PATCH 064/156] use a better error message --- pytential/symbolic/pde/system_utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index e2a72e70a..2a8cda438 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -422,8 +422,9 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, order = pde.order if order > pde.order: - raise RuntimeError(f"order ({order}) cannot be greater than the order" - f"of the PDE ({pde.order}) yet.") + raise NotImplementedError("Computing derivative relation when " + "the base kernel's derivatives are linearly dependent has not" + "been implemented yet.") mis = sorted(gnitstam(order, dim), key=sum) # (-1, -1, -1) represent a constant @@ -476,7 +477,12 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, if failed: if retries == 0: - raise RuntimeError("Failed to find a base kernel") + # The derivatives of the base kernel are not linearly + # independent. + # TODO: Extract a linearly independent set and return them + raise NotImplementedError("Computing derivative relation when " + "the base kernel's derivatives are linearly dependent has not " + "been implemented yet.") return _get_base_kernel_matrix( base_kernel=base_kernel, hashable_kernel_arguments=hashable_kernel_arguments, From 9c711cedcee4e1645b69de6ad4b94320c73375d7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 02:48:17 -0600 Subject: [PATCH 065/156] rename convert_int_g_to_base -> rewrite_int_g_using_base_kernel --- doc/symbolic.rst | 2 +- pytential/symbolic/pde/system_utils.py | 14 +++++++------- test/test_pde_system_utils.py | 11 +++++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/doc/symbolic.rst b/doc/symbolic.rst index 1e667a6a2..cd61348bf 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -72,5 +72,5 @@ Rewriting expressions with IntGs internals ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automethod:: pytential.symbolic.pde.system_utils.convert_target_transformation_to_source -.. automethod:: pytential.symbolic.pde.system_utils.convert_int_g_to_base +.. automethod:: pytential.symbolic.pde.system_utils.rewrite_int_g_using_base_kernel diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 2a8cda438..bd63e44ff 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -32,7 +32,7 @@ as gnitstam) from pytential.symbolic.primitives import (NodeCoordinateComponent, - hashable_kernel_args, IntG, as_dofdesc, TAG_WITH_DEFAULT_SOURCE) + hashable_kernel_args, IntG, TAG_WITH_DEFAULT_SOURCE) from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_with_post_division_callback import pytential @@ -103,7 +103,7 @@ def map_int_g(self, expr): new_int_gs = convert_target_transformation_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, + return sum(rewrite_int_g_using_base_kernel(new_int_g, self.base_kernel) for new_int_g in new_int_gs) @@ -273,22 +273,22 @@ def _multiply_int_g(int_g: IntG, expr_multiplier: sym.Basic, return result -def convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ +def rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) \ -> ExpressionT: - """Converts an *IntG* to an expression with *IntG*s having the + """Rewrite an *IntG* to an expression with *IntG*s having the base kernel *base_kernel*. """ result = 0 for knl, density in zip(int_g.source_kernels, int_g.densities): - result += _convert_int_g_to_base( + result += _rewrite_int_g_using_base_kernel( int_g.copy(source_kernels=(knl,), densities=(density,)), base_kernel) return result -def _convert_int_g_to_base(int_g: IntG, base_kernel: ExpressionKernel) \ +def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) \ -> ExpressionT: - """Converts an *IntG* with only one source kernel to an expression with *IntG*s + """Rewrites an *IntG* with only one source kernel to an expression with *IntG*s having the base kernel *base_kernel*. """ target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 037f375a1..44b016354 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -19,7 +19,7 @@ """ from pytential.symbolic.pde.system_utils import ( - convert_target_transformation_to_source, convert_int_g_to_base) + convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) from pytential.symbolic.primitives import IntG from pytential.symbolic.primitives import NodeCoordinateComponent import pytential @@ -130,7 +130,8 @@ def test_convert_int_g_base(): IntG(base_knl, [AxisSourceDerivative(d, AxisSourceDerivative(d, base_knl))], [-1], qbx_forced_limit=1) for d in range(3)) - assert expected_int_g == convert_int_g_to_base(int_g, base_kernel=base_knl) + assert expected_int_g == rewrite_int_g_using_base_kernel(int_g, + base_kernel=base_knl) def test_convert_int_g_base_with_const(): @@ -147,7 +148,8 @@ def test_convert_int_g_base_with_const(): IntG(base_knl, [AxisSourceDerivative(1, AxisSourceDerivative(1, base_knl))], [0.5], qbx_forced_limit=1) - assert convert_int_g_to_base(int_g, base_kernel=base_knl) == expected_int_g + assert rewrite_int_g_using_base_kernel(int_g, + base_kernel=base_knl) == expected_int_g def test_convert_int_g_base_with_const_and_deriv(): @@ -160,4 +162,5 @@ def test_convert_int_g_base_with_const_and_deriv(): [AxisSourceDerivative(1, AxisSourceDerivative(1, AxisSourceDerivative(0, base_knl)))], [0.5], qbx_forced_limit=1) - assert convert_int_g_to_base(int_g, base_kernel=base_knl) == expected_int_g + assert rewrite_int_g_using_base_kernel(int_g, + base_kernel=base_knl) == expected_int_g From 75d485ee7cf329f22286722077b0085368c12abd Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 23 Nov 2022 10:07:11 -0600 Subject: [PATCH 066/156] warning unknown target kernel --- pytential/symbolic/pde/system_utils.py | 2 ++ test/test_pde_system_utils.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index bd63e44ff..d8e1a887f 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -172,6 +172,8 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: expr = expr.diff(ds[knl.axis]) found = True else: + import warnings + warnings.warn(f"Unknown target kernel ({knl}) found.") return [int_g] knl = knl.inner_kernel diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 44b016354..7e91f6927 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -140,8 +140,7 @@ def test_convert_int_g_base_with_const(): base_knl = BiharmonicKernel(2) dim = 2 - dd = pytential.sym.DOFDescriptor(None, - discr_stage=pytential.sym.QBX_SOURCE_STAGE1) + dd = pytential.sym.DOFDescriptor(geometry=pytential.sym.TAG_WITH_DEFAULT_SOURCE) expected_int_g = (-0.1875)*prim.Power(np.pi, -1) * \ pytential.sym.integral(dim, dim-1, 1, dofdesc=dd) + \ From 2ac5f30db9f184adac880889fc7ab2d50f491555 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Dec 2022 12:38:41 -0600 Subject: [PATCH 067/156] add type hints for chop and lu_with_post_division_callback --- pytential/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pytential/utils.py b/pytential/utils.py index 8286cf8d5..fe18b3d6b 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -23,6 +23,7 @@ """ import sumpy.symbolic as sym +from typing import Iterable, Callable def sort_arrays_together(*arys, key=None): @@ -35,7 +36,7 @@ def sort_arrays_together(*arys, key=None): return zip(*sorted(zip(*arys), key=key)) -def chop(expr, tol): +def chop(expr: sym.Basic, tol) -> sym.Basic: """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 @@ -54,7 +55,13 @@ def chop(expr, tol): return expr.xreplace(replace_dict) -def lu_with_post_division_callback(L, U, perm, b, callback): +def lu_with_post_division_callback( + L: sym.Matrix, + U: sym.Matrix, + perm: Iterable[sym.Matrix], + b: Iterable[sym.Matrix], + callback: Callable[[sym.Basic], sym.Basic] + ) -> sym.Matrix: """Given an LU factorization and a vector, solve a linear system with intermediate results expanded to avoid an explosion of the expression trees From 1c95bc7ed63e0f5eaf5f607ca73af660739d8a37 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Dec 2022 13:56:26 -0600 Subject: [PATCH 068/156] use sym.nodes --- test/test_pde_system_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 7e91f6927..4ae1c2240 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -21,7 +21,7 @@ from pytential.symbolic.pde.system_utils import ( convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) from pytential.symbolic.primitives import IntG -from pytential.symbolic.primitives import NodeCoordinateComponent +import pytential.symbolic as sym import pytential import numpy as np @@ -47,7 +47,7 @@ def test_convert_target_deriv(): def test_convert_target_point_multiplier(): - xs = [NodeCoordinateComponent(i) for i in range(3)] + xs = sym.nodes(3).as_vector() knl = LaplaceKernel(3) int_g = IntG(TargetPointMultiplier(0, knl), [AxisSourceDerivative(1, knl), knl], @@ -74,7 +74,7 @@ def test_convert_target_point_multiplier(): def test_product_rule(): - xs = [NodeCoordinateComponent(i) for i in range(3)] + xs = sym.nodes(3).as_vector() knl = LaplaceKernel(3) int_g = IntG(AxisTargetDerivative(0, TargetPointMultiplier(0, knl)), [knl], [1], @@ -94,7 +94,7 @@ def test_product_rule(): def test_convert_helmholtz(): - xs = [NodeCoordinateComponent(i) for i in range(3)] + xs = sym.nodes(3).as_vector() knl = HelmholtzKernel(3) int_g = IntG(TargetPointMultiplier(0, knl), [knl], [1], From d710cd9f0eec2b2582f815af24b67c30520af8e7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Dec 2022 13:58:27 -0600 Subject: [PATCH 069/156] Use StokesletKernel when nu_sym == 0.5 --- pytential/symbolic/elasticity.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 086fd8a08..116f3b879 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -27,7 +27,7 @@ from pytential import sym from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel -from sumpy.kernel import (StressletKernel, LaplaceKernel, +from sumpy.kernel import (StressletKernel, LaplaceKernel, StokesletKernel, ElasticityKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant @@ -205,17 +205,18 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): self.base_kernel = base_kernel 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 # 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, dim): - self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, - jcomp=j, poisson_ratio=poisson_ratio) + if nu_sym == 0.5: + self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, + jcomp=j) + else: + self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, + jcomp=j) self.kernel_dict[(j, i)] = self.kernel_dict[(i, j)] def _get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, From 3a86bbc8c591edfdab03376ae4b9946cf7146681 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Dec 2022 15:34:12 -0600 Subject: [PATCH 070/156] Use DEFAULT_SOURCE --- pytential/symbolic/dof_desc.py | 10 ---------- pytential/symbolic/mappers.py | 4 ---- pytential/symbolic/pde/system_utils.py | 4 ++-- pytential/symbolic/primitives.py | 1 - 4 files changed, 2 insertions(+), 17 deletions(-) diff --git a/pytential/symbolic/dof_desc.py b/pytential/symbolic/dof_desc.py index 2abb7bc3a..2660f3b1f 100644 --- a/pytential/symbolic/dof_desc.py +++ b/pytential/symbolic/dof_desc.py @@ -77,16 +77,6 @@ class DEFAULT_TARGET: # noqa: N801 :func:`pytential.bind`.""" -class TAG_WITH_DEFAULT_SOURCE: # noqa: N801 - """Symbolic identifier for a source that is tagged with - the default source.""" - - -class TAG_WITH_DEFAULT_TARGET: # noqa: N801 - """Symbolic identifier for a source that is tagged with - the default target.""" - - class QBX_SOURCE_STAGE1: # noqa: N801 """Symbolic identifier for the Stage 1 discretization of a :class:`pytential.qbx.QBXLayerPotentialSource`. diff --git a/pytential/symbolic/mappers.py b/pytential/symbolic/mappers.py index 5c29e5516..d0e726e71 100644 --- a/pytential/symbolic/mappers.py +++ b/pytential/symbolic/mappers.py @@ -290,10 +290,6 @@ def _default_dofdesc(self, dofdesc): dofdesc = dofdesc.copy(geometry=self.default_target) else: dofdesc = dofdesc.copy(geometry=self.default_source) - elif dofdesc.geometry is prim.TAG_WITH_DEFAULT_SOURCE: - dofdesc = dofdesc.copy(geometry=self.default_source) - elif dofdesc.geometry is prim.TAG_WITH_DEFAULT_TARGET: - dofdesc = dofdesc.copy(geometry=self.default_target) return dofdesc diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index d8e1a887f..0e0f76e28 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -32,7 +32,7 @@ as gnitstam) from pytential.symbolic.primitives import (NodeCoordinateComponent, - hashable_kernel_args, IntG, TAG_WITH_DEFAULT_SOURCE) + hashable_kernel_args, IntG, DEFAULT_SOURCE) from pytential.symbolic.mappers import IdentityMapper from pytential.utils import chop, lu_with_post_division_callback import pytential @@ -309,7 +309,7 @@ def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) # on the source instead of the target when using automatic tagging # see :meth:`pytential.symbolic.mappers.LocationTagger._default_dofdesc` if int_g.source.geometry is None: - dd = int_g.source.copy(geometry=TAG_WITH_DEFAULT_SOURCE) + dd = int_g.source.copy(geometry=DEFAULT_SOURCE) else: dd = int_g.source const *= pytential.sym.integral(dim, dim-1, density, dofdesc=dd) diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index def661265..869a4e7f7 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -41,7 +41,6 @@ from pytential.symbolic.dof_desc import ( DEFAULT_SOURCE, DEFAULT_TARGET, - TAG_WITH_DEFAULT_SOURCE, TAG_WITH_DEFAULT_TARGET, QBX_SOURCE_STAGE1, QBX_SOURCE_STAGE2, QBX_SOURCE_QUAD_STAGE2, GRANULARITY_NODE, GRANULARITY_CENTER, GRANULARITY_ELEMENT, DOFDescriptor, DOFDescriptorLike, From 0c0e651ca9186461011093be0d68257a42f8b542 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 5 Dec 2022 17:03:50 -0600 Subject: [PATCH 071/156] Fix test --- test/test_pde_system_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 4ae1c2240..ae80975be 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -21,7 +21,7 @@ from pytential.symbolic.pde.system_utils import ( convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) from pytential.symbolic.primitives import IntG -import pytential.symbolic as sym +from pytential import sym import pytential import numpy as np @@ -140,7 +140,7 @@ def test_convert_int_g_base_with_const(): base_knl = BiharmonicKernel(2) dim = 2 - dd = pytential.sym.DOFDescriptor(geometry=pytential.sym.TAG_WITH_DEFAULT_SOURCE) + dd = pytential.sym.DOFDescriptor(geometry=pytential.sym.DEFAULT_SOURCE) expected_int_g = (-0.1875)*prim.Power(np.pi, -1) * \ pytential.sym.integral(dim, dim-1, 1, dofdesc=dd) + \ From cfdd1a505d0215b642e2d4c67df29ffdf56a0fbb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 12:56:51 -0600 Subject: [PATCH 072/156] Use _acf --- test/test_stokes.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 59bc9a01a..74477ca8b 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -763,14 +763,6 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): if __name__ == "__main__": import sys if len(sys.argv) > 1: - import pyopencl as cl - from arraycontext import PyOpenCLArrayContext - context = cl._csc() - queue = cl.CommandQueue(context) - - def actx_factory(): - return PyOpenCLArrayContext(queue) - exec(sys.argv[1]) else: from pytest import main From 7daeaacdac5345887e5b22a0c7f4a599acfaec2c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 12:58:10 -0600 Subject: [PATCH 073/156] Stokeslet -> Elasticity --- pytential/symbolic/elasticity.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 116f3b879..40c664c09 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -79,10 +79,10 @@ def __init__(self, dim, mu_sym, nu_sym): @abstractmethod def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): - """Symbolic expressions for integrating Stokeslet kernel. + """Symbolic expressions for integrating Elasticity kernel. Returns an object array of symbolic expressions for the vector - resulting from integrating the dyadic Stokeslet kernel with + resulting from integrating the dyadic Elasticity kernel with variable *density_vec_sym*. :arg density_vec_sym: a symbolic vector variable for the density vector. @@ -94,11 +94,11 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): raise NotImplementedError def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): - """Symbolic derivative of velocity from Stokeslet. + """Symbolic derivative of velocity from Elasticity kernel. 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*. + dyadic Elasticity 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. @@ -156,11 +156,11 @@ def apply(self, density_vec_sym, dir_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. + """Symbolic derivative of velocity from Elasticity kernel. 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*. + dyadic Elasticity 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. From 8ec658362f797b0cd894aeccdbb3d3cbbc4c922f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 12:58:39 -0600 Subject: [PATCH 074/156] No need to raise for abstractmethod --- pytential/symbolic/elasticity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 40c664c09..d1b625047 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -91,7 +91,6 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): :arg extra_deriv_dirs: adds target derivatives to all the integral objects with the given derivative axis. """ - raise NotImplementedError def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): """Symbolic derivative of velocity from Elasticity kernel. From 57a1824bdf4d03238e36f00aa63aa6cdd7ad7833 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:01:40 -0600 Subject: [PATCH 075/156] say which unsupported dim --- pytential/symbolic/elasticity.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index d1b625047..56a09161d 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -199,7 +199,8 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): self.nu = nu_sym if not (dim == 3 or dim == 2): - raise ValueError("unsupported dimension given to ElasticityWrapper") + raise ValueError( + f"unsupported dimension given to ElasticityWrapper: {dim}") self.base_kernel = base_kernel @@ -275,7 +276,7 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to " - "ElasticityDoubleLayerWrapper") + f"ElasticityDoubleLayerWrapper: {dim}") self.base_kernel = base_kernel @@ -488,7 +489,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " - "ElasticityDoubleLayerWrapperYoshida") + "ElasticityDoubleLayerWrapperYoshida: {dim}") self.kernel = LaplaceKernel(dim=3) self.mu = mu_sym self.nu = nu_sym @@ -600,7 +601,7 @@ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " - "ElasticityWrapperYoshida") + "ElasticityWrapperYoshida: {dim}") self.kernel = LaplaceKernel(dim=3) self.mu = mu_sym self.nu = nu_sym From d5eaad3d65edaffef83c9f9c4fcad7543f5e605c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:06:24 -0600 Subject: [PATCH 076/156] create_elasticity -> make_elasticity --- pytential/symbolic/elasticity.py | 8 ++++---- test/test_stokes.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 56a09161d..ce355e63c 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -38,8 +38,8 @@ .. autoclass:: ElasticityWrapperBase .. autoclass:: ElasticityDoubleLayerWrapperBase -.. automethod:: pytential.symbolic.elasticity.create_elasticity_wrapper -.. automethod:: pytential.symbolic.elasticity.create_elasticity_double_layer_wrapper +.. automethod:: pytential.symbolic.elasticity.make_elasticity_wrapper +.. automethod:: pytential.symbolic.elasticity.make_elasticity_double_layer_wrapper """ @@ -394,7 +394,7 @@ def __init__(self, dim, mu_sym, nu_sym): # {{{ dispatch function -def create_elasticity_wrapper( +def make_elasticity_wrapper( dim: int, mu_sym: ExpressionT = _MU_SYM_DEFAULT, nu_sym: ExpressionT = _NU_SYM_DEFAULT, @@ -431,7 +431,7 @@ def create_elasticity_wrapper( "Needs to be one of naive, laplace, biharmonic") -def create_elasticity_double_layer_wrapper( +def make_elasticity_double_layer_wrapper( dim: int, mu_sym: ExpressionT = _MU_SYM_DEFAULT, nu_sym: ExpressionT = _NU_SYM_DEFAULT, diff --git a/test/test_stokes.py b/test/test_stokes.py index 74477ca8b..e5dee3f0b 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -28,8 +28,8 @@ from arraycontext import flatten from pytential import GeometryCollection, bind, sym from pytential.symbolic.stokes import StokesletWrapper -from pytential.symbolic.elasticity import (create_elasticity_wrapper, - create_elasticity_double_layer_wrapper, +from pytential.symbolic.elasticity import (make_elasticity_wrapper, + make_elasticity_double_layer_wrapper, ElasticityDoubleLayerWrapperBase) from meshmode.discretization import Discretization from meshmode.discretization.poly_element import \ @@ -165,9 +165,9 @@ def run_exterior_stokes(actx_factory, *, else: sym_nu = SpatialConstant("nu2") - stokeslet = create_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, + stokeslet = make_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, nu_sym=sym_nu, method=method) - stresslet = create_elasticity_double_layer_wrapper(ambient_dim, mu_sym=sym_mu, + stresslet = make_elasticity_double_layer_wrapper(ambient_dim, mu_sym=sym_mu, nu_sym=sym_nu, method=method) if ambient_dim == 2: @@ -194,7 +194,7 @@ def run_exterior_stokes(actx_factory, *, else: # Use the naive method here as biharmonic requires source derivatives # of point_source - sym_source_pot = create_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, + sym_source_pot = make_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -682,7 +682,7 @@ def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): else: pde_class = ElasticityPDE - identity = pde_class(dim, create_elasticity_wrapper( + identity = pde_class(dim, make_elasticity_wrapper( case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) for resolution in resolutions: @@ -729,7 +729,7 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): else: pde_class = ElasticityPDE - identity = pde_class(dim, create_elasticity_double_layer_wrapper( + identity = pde_class(dim, make_elasticity_double_layer_wrapper( case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) from pytools.convergence import EOCRecorder From 4d45815f0f25e579309f7389627cf589288b6d20 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:21:48 -0600 Subject: [PATCH 077/156] Fix DOI links and add yoshida method to internals --- doc/symbolic.rst | 6 ++++++ pytential/symbolic/elasticity.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/symbolic.rst b/doc/symbolic.rst index cd61348bf..7cbe52b9e 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -48,6 +48,12 @@ Scalar Beltrami equations .. automodule:: pytential.symbolic.pde.beltrami +Internals +^^^^^^^^^ + +.. autoclass:: pytential.symbolic.elasticity.ElasticityWrapperYoshida +.. autoclass:: pytential.symbolic.elasticity.ElasticityDoubleLayerWrapperYoshida + Rewriting expressions with IntGs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index ce355e63c..556d39b85 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -475,14 +475,14 @@ def make_elasticity_double_layer_wrapper( # {{{ Yoshida class ElasticityDoubleLayerWrapperYoshida(ElasticityDoubleLayerWrapperBase): - """ElasticityDoubleLayer Wrapper using Yoshida et al's method [1] which uses + r"""ElasticityDoubleLayer 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. - https://doi.org/10.1002/1097-0207(20010130)50:3<525::AID-NME34>3.0.CO;2-4 + `DOI 3.0.CO;2-4>`__ # noqa """ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): @@ -587,14 +587,14 @@ def Q(i, int_g): class ElasticityWrapperYoshida(ElasticityWrapperBase): - """Elasticity single layer using Yoshida et al's method [1] which uses Laplace + r"""Elasticity single layer 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. - https://doi.org/10.1002/1097-0207(20010130)50:3<525::AID-NME34>3.0.CO;2-4 + `DOI 3.0.CO;2-4>`__ # noqa """ def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): From 926c52468a1daa8b72bc1b3c25b4efdc7eb5ba69 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:27:51 -0600 Subject: [PATCH 078/156] Improve docs about assumptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- 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 0e0f76e28..705aa03c0 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -64,7 +64,10 @@ def rewrite_using_base_kernel(exprs: List[ExpressionT], """ Rewrites a list of expressions with :class:`~pytential.symbolic.primitives.IntG` objects using *base_kernel*. Assumes that potentials are smooth, i.e. that - Schwarz's theorem holds. + Schwarz's theorem holds. If applied to on-surface evaluation, then the layer + potentials to which this is applied must be one-sided limits, and the potential + must be non-singular (as might occur due to corners). + 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 From 7167e6f13ab089e0f33f36dd13f591d7e48069f3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:29:12 -0600 Subject: [PATCH 079/156] Fix IntG in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- 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 705aa03c0..1791201fc 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -293,7 +293,7 @@ def rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) \ -> ExpressionT: - """Rewrites an *IntG* with only one source kernel to an expression with *IntG*s + r"""Rewrites an ``IntG`` with only one source kernel to an expression with ``IntG``\ s having the base kernel *base_kernel*. """ target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) From ba915a84eba4bfa73eebb93632f2aaba9c9945ee Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:34:42 -0600 Subject: [PATCH 080/156] Fix IntG markup --- doc/symbolic.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/symbolic.rst b/doc/symbolic.rst index 7cbe52b9e..f5b9f24b3 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -54,8 +54,8 @@ Internals .. autoclass:: pytential.symbolic.elasticity.ElasticityWrapperYoshida .. autoclass:: pytential.symbolic.elasticity.ElasticityDoubleLayerWrapperYoshida -Rewriting expressions with IntGs -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Rewriting expressions with ``IntG``\ s +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: pytential.symbolic.pde.system_utils @@ -74,8 +74,8 @@ How a symbolic operator gets executed .. automodule:: pytential.symbolic.compiler -Rewriting expressions with IntGs internals -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Rewriting expressions with ``IntG``\ s internals +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automethod:: pytential.symbolic.pde.system_utils.convert_target_transformation_to_source .. automethod:: pytential.symbolic.pde.system_utils.rewrite_int_g_using_base_kernel From 522021aab8873ef31000ba4264d2ffffb1d09d08 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:35:53 -0600 Subject: [PATCH 081/156] fix formatting in docstrings --- 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 1791201fc..53a6037de 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -67,7 +67,6 @@ def rewrite_using_base_kernel(exprs: List[ExpressionT], Schwarz's theorem holds. If applied to on-surface evaluation, then the layer potentials to which this is applied must be one-sided limits, and the potential must be non-singular (as might occur due to corners). - 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 @@ -293,8 +292,8 @@ def rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) \ -> ExpressionT: - r"""Rewrites an ``IntG`` with only one source kernel to an expression with ``IntG``\ s - having the base kernel *base_kernel*. + r"""Rewrites an ``IntG`` with only one source kernel to an expression with + ``IntG``\ s having the base kernel *base_kernel*. """ target_kernel = int_g.target_kernel.replace_base_kernel(base_kernel) dim = target_kernel.dim From 87c0e92fdd58256a07bbb1c324e12e8b44c32e27 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:45:54 -0600 Subject: [PATCH 082/156] dataclass --- pytential/symbolic/elasticity.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 556d39b85..adae585f4 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -32,6 +32,7 @@ AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant from abc import ABC, abstractmethod +from dataclasses import dataclass from pytential.symbolic.typing import ExpressionT __doc__ = """ @@ -49,6 +50,7 @@ _NU_SYM_DEFAULT = SpatialConstant("nu") +@dataclass class ElasticityWrapperBase(ABC): """Wrapper class for the :class:`~sumpy.kernel.ElasticityKernel` kernel. @@ -72,10 +74,9 @@ class ElasticityWrapperBase(ABC): .. automethod:: apply .. automethod:: apply_derivative """ - def __init__(self, dim, mu_sym, nu_sym): - self.dim = dim - self.mu = mu_sym - self.nu = nu_sym + dim: int + mu: ExpressionT + nu: ExpressionT @abstractmethod def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -107,6 +108,7 @@ def apply_derivative(self, deriv_dir, density_vec_sym, qbx_forced_limit): return self.apply(density_vec_sym, qbx_forced_limit, (deriv_dir,)) +@dataclass class ElasticityDoubleLayerWrapperBase(ABC): """Wrapper class for the double layer of :class:`~sumpy.kernel.ElasticityKernel` kernel. @@ -130,10 +132,9 @@ class ElasticityDoubleLayerWrapperBase(ABC): .. automethod:: apply .. automethod:: apply_derivative """ - def __init__(self, dim, mu_sym, nu_sym): - self.dim = dim - self.mu = mu_sym - self.nu = nu_sym + dim: int + mu: ExpressionT + nu: ExpressionT @abstractmethod def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, From 5bfe80b33c6ad704ac8837af3c7fb27dbcf9cc78 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 13:46:48 -0600 Subject: [PATCH 083/156] mu_sym -> mu --- pytential/symbolic/elasticity.py | 102 +++++++++++++++---------------- pytential/symbolic/stokes.py | 90 +++++++++++++-------------- test/test_stokes.py | 20 +++--- 3 files changed, 106 insertions(+), 106 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index adae585f4..6fad04ef1 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -194,10 +194,10 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): class _ElasticityWrapperNaiveOrBiharmonic: - def __init__(self, dim, mu_sym, nu_sym, base_kernel): + def __init__(self, dim, mu, nu, base_kernel): self.dim = dim - self.mu = mu_sym - self.nu = nu_sym + self.mu = mu + self.nu = nu if not (dim == 3 or dim == 2): raise ValueError( @@ -212,7 +212,7 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): # multiple expansions for the same kernel in a different ordering. for i in range(dim): for j in range(i, dim): - if nu_sym == 0.5: + if nu == 0.5: self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, jcomp=j) else: @@ -250,17 +250,17 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): class ElasticityWrapperNaive(_ElasticityWrapperNaiveOrBiharmonic, ElasticityWrapperBase): - def __init__(self, dim, mu_sym, nu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, base_kernel=None) - ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + def __init__(self, dim, mu, nu): + super().__init__(dim=dim, mu=mu, nu=nu, base_kernel=None) + ElasticityWrapperBase.__init__(self, dim=dim, mu=mu, nu=nu) class ElasticityWrapperBiharmonic(_ElasticityWrapperNaiveOrBiharmonic, ElasticityWrapperBase): - def __init__(self, dim, mu_sym, nu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + def __init__(self, dim, mu, nu): + super().__init__(dim=dim, mu=mu, nu=nu, base_kernel=BiharmonicKernel(dim)) - ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + ElasticityWrapperBase.__init__(self, dim=dim, mu=mu, nu=nu) # }}} @@ -270,10 +270,10 @@ def __init__(self, dim, mu_sym, nu_sym): class _ElasticityDoubleLayerWrapperNaiveOrBiharmonic: - def __init__(self, dim, mu_sym, nu_sym, base_kernel): + def __init__(self, dim, mu, nu, base_kernel): self.dim = dim - self.mu = mu_sym - self.nu = nu_sym + self.mu = mu + self.nu = nu if not (dim == 3 or dim == 2): raise ValueError("unsupported dimension given to " @@ -301,7 +301,7 @@ def __init__(self, dim, mu_sym, nu_sym, base_kernel): self.kernel_dict[(i, j, k)] = self.kernel_dict[s] # For elasticity (nu != 0.5), we need the LaplaceKernel - if nu_sym != 0.5: + if nu != 0.5: self.kernel_dict["laplace"] = LaplaceKernel(self.dim) def _get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, @@ -358,7 +358,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, extra_deriv_dirs=()): stokeslet_obj = _ElasticityWrapperNaiveOrBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=self.nu, base_kernel=self.base_kernel) + mu=self.mu, nu=self.nu, base_kernel=self.base_kernel) sym_expr = 0 if stresslet_weight != 0: @@ -374,21 +374,21 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, class ElasticityDoubleLayerWrapperNaive( _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, ElasticityDoubleLayerWrapperBase): - def __init__(self, dim, mu_sym, nu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + def __init__(self, dim, mu, nu): + super().__init__(dim=dim, mu=mu, nu=nu, base_kernel=None) ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu=mu, nu=nu) class ElasticityDoubleLayerWrapperBiharmonic( _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, ElasticityDoubleLayerWrapperBase): - def __init__(self, dim, mu_sym, nu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym, + def __init__(self, dim, mu, nu): + super().__init__(dim=dim, mu=mu, nu=nu, base_kernel=BiharmonicKernel(dim)) ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu=mu, nu=nu) # }}} @@ -397,36 +397,36 @@ def __init__(self, dim, mu_sym, nu_sym): def make_elasticity_wrapper( dim: int, - mu_sym: ExpressionT = _MU_SYM_DEFAULT, - nu_sym: ExpressionT = _NU_SYM_DEFAULT, + mu: ExpressionT = _MU_SYM_DEFAULT, + nu: ExpressionT = _NU_SYM_DEFAULT, method: str = "naive") -> ElasticityWrapperBase: """Creates a :class:`ElasticityWrapperBase` object depending on the input values. :param: dim: dimension - :param: mu_sym: viscosity symbol, defaults to "mu" - :param: nu_sym: poisson ratio symbol, defaults to "nu" + :param: mu: viscosity symbol, defaults to "mu" + :param: nu: poisson ratio symbol, defaults to "nu" :param: method: method to use, defaults to "naive". One of ("naive", "laplace", "biharmonic") :return: a :class:`ElasticityWrapperBase` object """ - if nu_sym == 0.5: + if nu == 0.5: from pytential.symbolic.stokes import StokesletWrapper - return StokesletWrapper(dim=dim, mu_sym=mu_sym, method=method) + return StokesletWrapper(dim=dim, mu=mu, method=method) if method == "naive": - return ElasticityWrapperNaive(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + return ElasticityWrapperNaive(dim=dim, mu=mu, nu=nu) elif method == "biharmonic": - return ElasticityWrapperBiharmonic(dim=dim, mu_sym=mu_sym, nu_sym=nu_sym) + return ElasticityWrapperBiharmonic(dim=dim, mu=mu, nu=nu) elif method == "laplace": - if nu_sym == 0.5: + if nu == 0.5: from pytential.symbolic.stokes import StokesletWrapperTornberg return StokesletWrapperTornberg(dim=dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu=mu, nu=nu) else: return ElasticityWrapperYoshida(dim=dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu=mu, nu=nu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -434,37 +434,37 @@ def make_elasticity_wrapper( def make_elasticity_double_layer_wrapper( dim: int, - mu_sym: ExpressionT = _MU_SYM_DEFAULT, - nu_sym: ExpressionT = _NU_SYM_DEFAULT, + mu: ExpressionT = _MU_SYM_DEFAULT, + nu: ExpressionT = _NU_SYM_DEFAULT, method: str = "naive") -> ElasticityDoubleLayerWrapperBase: """Creates a :class:`ElasticityDoubleLayerWrapperBase` object depending on the input values. :param: dim: dimension - :param: mu_sym: viscosity symbol, defaults to "mu" - :param: nu_sym: poisson ratio symbol, defaults to "nu" + :param: mu: viscosity symbol, defaults to "mu" + :param: nu: poisson ratio symbol, defaults to "nu" :param: method: method to use, defaults to "naive". One of ("naive", "laplace", "biharmonic") :return: a :class:`ElasticityDoubleLayerWrapperBase` object """ - if nu_sym == 0.5: + if nu == 0.5: from pytential.symbolic.stokes import StressletWrapper - return StressletWrapper(dim=dim, mu_sym=mu_sym, method=method) + return StressletWrapper(dim=dim, mu=mu, method=method) if method == "naive": - return ElasticityDoubleLayerWrapperNaive(dim=dim, mu_sym=mu_sym, - nu_sym=nu_sym) + return ElasticityDoubleLayerWrapperNaive(dim=dim, mu=mu, + nu=nu) elif method == "biharmonic": - return ElasticityDoubleLayerWrapperBiharmonic(dim=dim, mu_sym=mu_sym, - nu_sym=nu_sym) + return ElasticityDoubleLayerWrapperBiharmonic(dim=dim, mu=mu, + nu=nu) elif method == "laplace": - if nu_sym == 0.5: + if nu == 0.5: from pytential.symbolic.stokes import StressletWrapperTornberg return StressletWrapperTornberg(dim=dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu=mu, nu=nu) else: return ElasticityDoubleLayerWrapperYoshida(dim=dim, - mu_sym=mu_sym, nu_sym=nu_sym) + mu=mu, nu=nu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -486,14 +486,14 @@ class ElasticityDoubleLayerWrapperYoshida(ElasticityDoubleLayerWrapperBase): `DOI 3.0.CO;2-4>`__ # noqa """ - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): + def __init__(self, dim=None, mu=_MU_SYM_DEFAULT, nu=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " "ElasticityDoubleLayerWrapperYoshida: {dim}") self.kernel = LaplaceKernel(dim=3) - self.mu = mu_sym - self.nu = nu_sym + self.mu = mu + self.nu = nu def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -598,14 +598,14 @@ class ElasticityWrapperYoshida(ElasticityWrapperBase): `DOI 3.0.CO;2-4>`__ # noqa """ - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=_NU_SYM_DEFAULT): + def __init__(self, dim=None, mu=_MU_SYM_DEFAULT, nu=_NU_SYM_DEFAULT): self.dim = dim if dim != 3: raise ValueError("unsupported dimension given to " "ElasticityWrapperYoshida: {dim}") self.kernel = LaplaceKernel(dim=3) - self.mu = mu_sym - self.nu = nu_sym + self.mu = mu + self.nu = nu self.stresslet = ElasticityDoubleLayerWrapperYoshida(3, self.mu, self.nu) def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index df06f49f8..e0d51e8dd 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -64,8 +64,8 @@ class StokesletWrapperBase(ElasticityWrapperBase): .. automethod:: apply_derivative .. automethod:: apply_stress """ - def __init__(self, dim, mu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5) + def __init__(self, dim, mu): + super().__init__(dim=dim, mu=mu, nu=0.5) def apply_pressure(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): """Symbolic expression for pressure field associated with the Stokeslet.""" @@ -122,8 +122,8 @@ class also provides :meth:`apply_stress` which applies symmetric viscous stress .. automethod:: apply_derivative .. automethod:: apply_stress """ - def __init__(self, dim, mu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5) + def __init__(self, dim, mu): + super().__init__(dim=dim, mu=mu, nu=0.5) def apply_pressure(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -191,7 +191,7 @@ def apply_stress(self, density_vec_sym, dir_vec_sym, qbx_forced_limit): sym_expr = np.zeros((self.dim,), dtype=object) stresslet_obj = _StressletWrapperNaiveOrBiharmonic(dim=self.dim, - mu_sym=self.mu, nu_sym=0.5, base_kernel=self.base_kernel) + mu=self.mu, nu=0.5, base_kernel=self.base_kernel) # For stokeslet, there's no direction vector involved # passing a list of ones instead to remove its usage. @@ -246,35 +246,35 @@ def apply_stress(self, density_vec_sym, normal_vec_sym, dir_vec_sym, class StokesletWrapperNaive(_StokesletWrapperNaiveOrBiharmonic, ElasticityWrapperBase): - def __init__(self, dim, mu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, base_kernel=None) - ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=0.5) + def __init__(self, dim, mu): + super().__init__(dim=dim, mu=mu, nu=0.5, base_kernel=None) + ElasticityWrapperBase.__init__(self, dim=dim, mu=mu, nu=0.5) class StressletWrapperNaive(_StressletWrapperNaiveOrBiharmonic, ElasticityDoubleLayerWrapperBase): - def __init__(self, dim, mu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, base_kernel=None) - ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, - nu_sym=0.5) + def __init__(self, dim, mu): + super().__init__(dim=dim, mu=mu, nu=0.5, base_kernel=None) + ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, mu=mu, + nu=0.5) class StokesletWrapperBiharmonic(_StokesletWrapperNaiveOrBiharmonic, ElasticityWrapperBase): - def __init__(self, dim, mu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, + def __init__(self, dim, mu): + super().__init__(dim=dim, mu=mu, nu=0.5, base_kernel=BiharmonicKernel(dim)) - ElasticityWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, nu_sym=0.5) + ElasticityWrapperBase.__init__(self, dim=dim, mu=mu, nu=0.5) class StressletWrapperBiharmonic(_StressletWrapperNaiveOrBiharmonic, ElasticityDoubleLayerWrapperBase): - def __init__(self, dim, mu_sym): - super().__init__(dim=dim, mu_sym=mu_sym, nu_sym=0.5, + def __init__(self, dim, mu): + super().__init__(dim=dim, mu=mu, nu=0.5, base_kernel=BiharmonicKernel(dim)) - ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, mu_sym=mu_sym, - nu_sym=0.5) + ElasticityDoubleLayerWrapperBase.__init__(self, dim=dim, mu=mu, + nu=0.5) # }}} @@ -290,13 +290,13 @@ class StokesletWrapperTornberg(StokesletWrapperBase): Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim=None, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + def __init__(self, dim=None, mu=_MU_SYM_DEFAULT, nu=0.5): self.dim = dim - if nu_sym != 0.5: + if nu != 0.5: raise ValueError("nu != 0.5 is not supported") self.kernel = LaplaceKernel(dim=self.dim) - self.mu = mu_sym - self.nu = nu_sym + self.mu = mu + self.nu = nu def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): stresslet = StressletWrapperTornberg(self.dim, self.mu, self.nu) @@ -313,13 +313,13 @@ class StressletWrapperTornberg(StressletWrapperBase): three-dimensional Stokes equations. Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim, mu_sym=_MU_SYM_DEFAULT, nu_sym=0.5): + def __init__(self, dim, mu=_MU_SYM_DEFAULT, nu=0.5): self.dim = dim - if nu_sym != 0.5: + if nu != 0.5: raise ValueError("nu != 0.5 is not supported") self.kernel = LaplaceKernel(dim=self.dim) - self.mu = mu_sym - self.nu = nu_sym + self.mu = mu + self.nu = nu def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -400,35 +400,35 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, # {{{ StokesletWrapper dispatch method -def StokesletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, method=None): # noqa: N806 +def StokesletWrapper(dim, mu=_MU_SYM_DEFAULT, method=None): # noqa: N806 if method is 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) + return StokesletWrapperNaive(dim=dim, mu=mu) elif method == "biharmonic": - return StokesletWrapperBiharmonic(dim=dim, mu_sym=mu_sym) + return StokesletWrapperBiharmonic(dim=dim, mu=mu) elif method == "laplace": - return StokesletWrapperTornberg(dim=dim, mu_sym=mu_sym) + return StokesletWrapperTornberg(dim=dim, mu=mu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") -def StressletWrapper(dim, mu_sym=_MU_SYM_DEFAULT, method=None): # noqa: N806 +def StressletWrapper(dim, mu=_MU_SYM_DEFAULT, method=None): # noqa: N806 if method is 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) + return StressletWrapperNaive(dim=dim, mu=mu) elif method == "biharmonic": - return StressletWrapperBiharmonic(dim=dim, mu_sym=mu_sym) + return StressletWrapperBiharmonic(dim=dim, mu=mu) elif method == "laplace": - return StressletWrapperTornberg(dim=dim, mu_sym=mu_sym) + return StressletWrapperTornberg(dim=dim, mu=mu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -452,7 +452,7 @@ class StokesOperator: .. automethod:: pressure """ - def __init__(self, ambient_dim, side, stokeslet, stresslet, mu_sym): + def __init__(self, ambient_dim, side, stokeslet, stresslet, mu): """ :arg ambient_dim: dimension of the ambient space. :arg side: :math:`+1` for exterior or :math:`-1` for interior. @@ -463,20 +463,20 @@ def __init__(self, ambient_dim, side, stokeslet, stresslet, mu_sym): self.ambient_dim = ambient_dim self.side = side - if mu_sym is not None: + if mu is not None: import warnings - warnings.warn("Explicitly giving mu_sym is deprecated. " + warnings.warn("Explicitly giving mu is deprecated. " "Use stokeslet and stresslet arguments.") else: - mu_sym = _MU_SYM_DEFAULT + mu = _MU_SYM_DEFAULT if stresslet is None: stresslet = StressletWrapper(dim=self.ambient_dim, - mu_sym=mu_sym) + mu=mu) if stokeslet is None: stokeslet = StokesletWrapper(dim=self.ambient_dim, - mu_sym=mu_sym) + mu=mu) self.stokeslet = stokeslet self.stresslet = stresslet @@ -539,7 +539,7 @@ class HsiaoKressExteriorStokesOperator(StokesOperator): """ def __init__(self, *, omega, alpha=1.0, eta=1.0, - stokeslet=None, stresslet=None, mu_sym=None): + stokeslet=None, stresslet=None, mu=None): r""" :arg omega: farfield behaviour of the velocity field, as defined by :math:`A` in [HsiaoKress1985]_ Equation 2.3. @@ -548,7 +548,7 @@ def __init__(self, *, omega, alpha=1.0, eta=1.0, can have a non-trivial effect on the conditioning. """ super().__init__(ambient_dim=2, side=+1, stokeslet=stokeslet, - stresslet=stresslet, mu_sym=mu_sym) + stresslet=stresslet, mu=mu) # NOTE: in [hsiao-kress], there is an analysis on a circle, which # recommends values in @@ -617,14 +617,14 @@ class HebekerExteriorStokesOperator(StokesOperator): .. automethod:: __init__ """ - def __init__(self, *, eta=None, stokeslet=None, stresslet=None, mu_sym=None): + def __init__(self, *, eta=None, stokeslet=None, stresslet=None, mu=None): 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, stokeslet=stokeslet, - stresslet=stresslet, mu_sym=mu_sym) + stresslet=stresslet, mu=mu) # 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_stokes.py b/test/test_stokes.py index e5dee3f0b..5a217cf72 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -165,10 +165,10 @@ def run_exterior_stokes(actx_factory, *, else: sym_nu = SpatialConstant("nu2") - stokeslet = make_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, - nu_sym=sym_nu, method=method) - stresslet = make_elasticity_double_layer_wrapper(ambient_dim, mu_sym=sym_mu, - nu_sym=sym_nu, method=method) + stokeslet = make_elasticity_wrapper(ambient_dim, mu=sym_mu, + nu=sym_nu, method=method) + stresslet = make_elasticity_double_layer_wrapper(ambient_dim, mu=sym_mu, + nu=sym_nu, method=method) if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator @@ -194,8 +194,8 @@ def run_exterior_stokes(actx_factory, *, else: # Use the naive method here as biharmonic requires source derivatives # of point_source - sym_source_pot = make_elasticity_wrapper(ambient_dim, mu_sym=sym_mu, - nu_sym=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) + sym_source_pot = make_elasticity_wrapper(ambient_dim, mu=sym_mu, + nu=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -467,7 +467,7 @@ class StokesletIdentity: def __init__(self, ambient_dim): self.ambient_dim = ambient_dim - self.stokeslet = StokesletWrapper(self.ambient_dim, mu_sym=1) + self.stokeslet = StokesletWrapper(self.ambient_dim, mu=1) def apply_operator(self): sym_density = sym.normal(self.ambient_dim).as_vector() @@ -525,7 +525,7 @@ class StressletIdentity: def __init__(self, ambient_dim): self.ambient_dim = ambient_dim - self.stokeslet = StokesletWrapper(self.ambient_dim, mu_sym=1) + self.stokeslet = StokesletWrapper(self.ambient_dim, mu=1) def apply_operator(self): sym_density = sym.normal(self.ambient_dim).as_vector() @@ -683,7 +683,7 @@ def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): pde_class = ElasticityPDE identity = pde_class(dim, make_elasticity_wrapper( - case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) + case.ambient_dim, mu=1, nu=nu, method=method)) for resolution in resolutions: h_max, errors = run_stokes_identity( @@ -730,7 +730,7 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): pde_class = ElasticityPDE identity = pde_class(dim, make_elasticity_double_layer_wrapper( - case.ambient_dim, mu_sym=1, nu_sym=nu, method=method)) + case.ambient_dim, mu=1, nu=nu, method=method)) from pytools.convergence import EOCRecorder eocs = [EOCRecorder() for _ in range(case.ambient_dim)] From 51aa0093a9ccf1aceca8f7f84a39440ec50eed50 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 14:13:04 -0600 Subject: [PATCH 084/156] Use dataclasses --- pytential/symbolic/elasticity.py | 122 +++++++++++++++++-------------- pytential/symbolic/stokes.py | 43 ++++++----- 2 files changed, 94 insertions(+), 71 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 6fad04ef1..8697908b4 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -28,11 +28,12 @@ from pytential import sym from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel from sumpy.kernel import (StressletKernel, LaplaceKernel, StokesletKernel, - ElasticityKernel, BiharmonicKernel, + ElasticityKernel, BiharmonicKernel, Kernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant from abc import ABC, abstractmethod from dataclasses import dataclass +from functools import cached_property from pytential.symbolic.typing import ExpressionT __doc__ = """ @@ -193,32 +194,35 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): return res +@dataclass class _ElasticityWrapperNaiveOrBiharmonic: - def __init__(self, dim, mu, nu, base_kernel): - self.dim = dim - self.mu = mu - self.nu = nu + dim: int + mu: ExpressionT + nu: ExpressionT + base_kernel: Kernel - if not (dim == 3 or dim == 2): + def __post_init__(self): + if not (self.dim == 3 or self.dim == 2): raise ValueError( - f"unsupported dimension given to ElasticityWrapper: {dim}") - - self.base_kernel = base_kernel - - self.kernel_dict = {} + f"unsupported dimension given to ElasticityWrapper: {self.dim}") + @cached_property + def kernel_dict(self): + d = {} # 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, dim): - if nu == 0.5: - self.kernel_dict[(i, j)] = StokesletKernel(dim=dim, icomp=i, + for i in range(self.dim): + for j in range(i, self.dim): + if self.nu == 0.5: + d[(i, j)] = StokesletKernel(dim=self.dim, icomp=i, jcomp=j) else: - self.kernel_dict[(i, j)] = ElasticityKernel(dim=dim, icomp=i, + d[(i, j)] = ElasticityKernel(dim=self.dim, icomp=i, jcomp=j) - self.kernel_dict[(j, i)] = self.kernel_dict[(i, j)] + d[(j, i)] = d[(i, j)] + + return d def _get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): @@ -268,41 +272,44 @@ def __init__(self, dim, mu, nu): # {{{ ElasticityDoubleLayerWrapper Naive and Biharmonic impl +@dataclass class _ElasticityDoubleLayerWrapperNaiveOrBiharmonic: + dim: int + mu: ExpressionT + nu: ExpressionT + base_kernel: Kernel - def __init__(self, dim, mu, nu, base_kernel): - self.dim = dim - self.mu = mu - self.nu = nu - - if not (dim == 3 or dim == 2): + def __post_init__(self): + if not (self.dim == 3 or self.dim == 2): raise ValueError("unsupported dimension given to " - f"ElasticityDoubleLayerWrapper: {dim}") + f"ElasticityDoubleLayerWrapper: {self.dim}") - self.base_kernel = base_kernel + @cached_property + def kernel_dict(self): + d = {} - self.kernel_dict = {} - - 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, + for i in range(self.dim): + for j in range(i, self.dim): + for k in range(j, self.dim): + d[(i, j, k)] = StressletKernel(dim=self.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): - if (i, j, k) in self.kernel_dict: + for i in range(self.dim): + for j in range(self.dim): + for k in range(self.dim): + if (i, j, k) in d: continue s = tuple(sorted([i, j, k])) - self.kernel_dict[(i, j, k)] = self.kernel_dict[s] + d[(i, j, k)] = d[s] # For elasticity (nu != 0.5), we need the LaplaceKernel - if nu != 0.5: - self.kernel_dict["laplace"] = LaplaceKernel(self.dim) + if self.nu != 0.5: + d["laplace"] = LaplaceKernel(self.dim) + + return d def _get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): @@ -475,6 +482,7 @@ def make_elasticity_double_layer_wrapper( # {{{ Yoshida +@dataclass class ElasticityDoubleLayerWrapperYoshida(ElasticityDoubleLayerWrapperBase): r"""ElasticityDoubleLayer Wrapper using Yoshida et al's method [1] which uses Laplace derivatives. @@ -485,15 +493,18 @@ class ElasticityDoubleLayerWrapperYoshida(ElasticityDoubleLayerWrapperBase): International Journal for Numerical Methods in Engineering, 50(3), 525-547. `DOI 3.0.CO;2-4>`__ # noqa """ + dim: int + mu: ExpressionT + nu: ExpressionT - def __init__(self, dim=None, mu=_MU_SYM_DEFAULT, nu=_NU_SYM_DEFAULT): - self.dim = dim - if dim != 3: + def __post_init__(self): + if not self.dim == 3: raise ValueError("unsupported dimension given to " - "ElasticityDoubleLayerWrapperYoshida: {dim}") - self.kernel = LaplaceKernel(dim=3) - self.mu = mu - self.nu = nu + "ElasticityDoubleLayerWrapperYoshida: {self.dim}") + + @cached_property + def laplace_kernel(self): + return LaplaceKernel(dim=3) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -542,7 +553,7 @@ def Q(i, int_g): sym_expr = np.zeros((3,), dtype=object) - kernel = self.kernel + kernel = self.laplace_kernel source = [sym.NodeCoordinateComponent(d) for d in range(3)] normal = dir_vec_sym sigma = stresslet_density_vec_sym @@ -587,6 +598,7 @@ def Q(i, int_g): return sym_expr +@dataclass class ElasticityWrapperYoshida(ElasticityWrapperBase): r"""Elasticity single layer using Yoshida et al's method [1] which uses Laplace derivatives. @@ -597,16 +609,18 @@ class ElasticityWrapperYoshida(ElasticityWrapperBase): International Journal for Numerical Methods in Engineering, 50(3), 525-547. `DOI 3.0.CO;2-4>`__ # noqa """ + dim: int + mu: ExpressionT + nu: ExpressionT - def __init__(self, dim=None, mu=_MU_SYM_DEFAULT, nu=_NU_SYM_DEFAULT): - self.dim = dim - if dim != 3: + def __post_init__(self): + if not self.dim == 3: raise ValueError("unsupported dimension given to " - "ElasticityWrapperYoshida: {dim}") - self.kernel = LaplaceKernel(dim=3) - self.mu = mu - self.nu = nu - self.stresslet = ElasticityDoubleLayerWrapperYoshida(3, self.mu, self.nu) + "ElasticityDoubleLayerWrapperYoshida: {self.dim}") + + @cached_property + def stresslet(self): + return ElasticityDoubleLayerWrapperYoshida(3, self.mu, self.nu) def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): return self.stresslet.apply_single_and_double_layer(density_vec_sym, diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index e0d51e8dd..849dda2ef 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -34,6 +34,9 @@ _ElasticityWrapperNaiveOrBiharmonic, _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, _MU_SYM_DEFAULT) +from pytential.symbolic.typing import ExpressionT +from dataclasses import dataclass +from functools import cached_property from abc import abstractmethod __doc__ = """ @@ -281,6 +284,7 @@ def __init__(self, dim, mu): # {{{ Stokeslet/Stresslet using Laplace (Tornberg) +@dataclass class StokesletWrapperTornberg(StokesletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -289,14 +293,13 @@ class StokesletWrapperTornberg(StokesletWrapperBase): three-dimensional Stokes equations. Journal of Computational Physics, 227(3), 1613-1619. """ + dim: int + mu: ExpressionT + nu: ExpressionT - def __init__(self, dim=None, mu=_MU_SYM_DEFAULT, nu=0.5): - self.dim = dim - if nu != 0.5: + def __post_init__(self): + if self.nu != 0.5: raise ValueError("nu != 0.5 is not supported") - self.kernel = LaplaceKernel(dim=self.dim) - self.mu = mu - self.nu = nu def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): stresslet = StressletWrapperTornberg(self.dim, self.mu, self.nu) @@ -305,6 +308,7 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): extra_deriv_dirs) +@dataclass class StressletWrapperTornberg(StressletWrapperBase): """A Stresslet wrapper using Tornberg and Greengard's method which uses Laplace derivatives. @@ -313,13 +317,17 @@ class StressletWrapperTornberg(StressletWrapperBase): three-dimensional Stokes equations. Journal of Computational Physics, 227(3), 1613-1619. """ - def __init__(self, dim, mu=_MU_SYM_DEFAULT, nu=0.5): - self.dim = dim - if nu != 0.5: + dim: int + mu: ExpressionT + nu: ExpressionT + + def __post_init__(self): + if self.nu != 0.5: raise ValueError("nu != 0.5 is not supported") - self.kernel = LaplaceKernel(dim=self.dim) - self.mu = mu - self.nu = nu + + @cached_property + def laplace_kernel(self): + return LaplaceKernel(dim=self.dim) def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): @@ -334,9 +342,10 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, sym_expr = np.zeros((self.dim,), dtype=object) source = sym.nodes(self.dim).as_vector() - common_source_kernels = [AxisSourceDerivative(k, self.kernel) for + common_source_kernels = [ + AxisSourceDerivative(k, self.laplace_kernel) for k in range(self.dim)] - common_source_kernels.append(self.kernel) + common_source_kernels.append(self.laplace_kernel) # The paper in [1] ignores the scaling we use Stokeslet/Stresslet # and gives formulae for the kernel expression only @@ -355,7 +364,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, for k in range(self.dim)] densities.append(stokeslet_weight*stokeslet_density_vec_sym[j]) target_kernel = TargetPointMultiplier(j, - AxisTargetDerivative(i, self.kernel)) + AxisTargetDerivative(i, self.laplace_kernel)) for deriv_dir in extra_deriv_dirs: target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) sym_expr[i] -= sym.IntG(target_kernel=target_kernel, @@ -364,7 +373,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, qbx_forced_limit=qbx_forced_limit) if i == j: - target_kernel = self.kernel + target_kernel = self.laplace_kernel for deriv_dir in extra_deriv_dirs: target_kernel = AxisTargetDerivative( deriv_dir, target_kernel) @@ -385,7 +394,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, k in range(self.dim)] densities.append(stokeslet_weight * common_density2) - target_kernel = AxisTargetDerivative(i, self.kernel) + target_kernel = AxisTargetDerivative(i, self.laplace_kernel) for deriv_dir in extra_deriv_dirs: target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) sym_expr[i] += sym.IntG(target_kernel=target_kernel, From c5d9c35dc16cdb91245674e5fa5090b67b2a5365 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 14:25:41 -0600 Subject: [PATCH 085/156] Use enum for method --- pytential/symbolic/elasticity.py | 33 ++++++++++++++++++++------------ pytential/symbolic/stokes.py | 30 ++++++++++++++++++----------- test/test_stokes.py | 12 ++++++------ 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 8697908b4..bf56ea89a 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -31,14 +31,17 @@ ElasticityKernel, BiharmonicKernel, Kernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant +from pytential.symbolic.typing import ExpressionT + from abc import ABC, abstractmethod from dataclasses import dataclass from functools import cached_property -from pytential.symbolic.typing import ExpressionT +from enum import Enum __doc__ = """ .. autoclass:: ElasticityWrapperBase .. autoclass:: ElasticityDoubleLayerWrapperBase +.. autoclass:: Method .. automethod:: pytential.symbolic.elasticity.make_elasticity_wrapper .. automethod:: pytential.symbolic.elasticity.make_elasticity_double_layer_wrapper @@ -402,19 +405,26 @@ def __init__(self, dim, mu, nu): # {{{ dispatch function +class Method(Enum): + """Method to use in Elasticity/Stokes problem. + """ + naive = 1 + laplace = 2 + biharmonic = 3 + + def make_elasticity_wrapper( dim: int, mu: ExpressionT = _MU_SYM_DEFAULT, nu: ExpressionT = _NU_SYM_DEFAULT, - method: str = "naive") -> ElasticityWrapperBase: + method: Method = Method.naive) -> ElasticityWrapperBase: """Creates a :class:`ElasticityWrapperBase` object depending on the input values. :param: dim: dimension :param: mu: viscosity symbol, defaults to "mu" :param: nu: poisson ratio symbol, defaults to "nu" - :param: method: method to use, defaults to "naive". - One of ("naive", "laplace", "biharmonic") + :param: method: method to use, defaults to naive. :return: a :class:`ElasticityWrapperBase` object """ @@ -422,11 +432,11 @@ def make_elasticity_wrapper( if nu == 0.5: from pytential.symbolic.stokes import StokesletWrapper return StokesletWrapper(dim=dim, mu=mu, method=method) - if method == "naive": + if method == Method.naive: return ElasticityWrapperNaive(dim=dim, mu=mu, nu=nu) - elif method == "biharmonic": + elif method == Method.biharmonic: return ElasticityWrapperBiharmonic(dim=dim, mu=mu, nu=nu) - elif method == "laplace": + elif method == Method.laplace: if nu == 0.5: from pytential.symbolic.stokes import StokesletWrapperTornberg return StokesletWrapperTornberg(dim=dim, @@ -443,7 +453,7 @@ def make_elasticity_double_layer_wrapper( dim: int, mu: ExpressionT = _MU_SYM_DEFAULT, nu: ExpressionT = _NU_SYM_DEFAULT, - method: str = "naive") -> ElasticityDoubleLayerWrapperBase: + method: Method = Method.naive) -> ElasticityDoubleLayerWrapperBase: """Creates a :class:`ElasticityDoubleLayerWrapperBase` object depending on the input values. @@ -451,20 +461,19 @@ def make_elasticity_double_layer_wrapper( :param: mu: viscosity symbol, defaults to "mu" :param: nu: poisson ratio symbol, defaults to "nu" :param: method: method to use, defaults to "naive". - One of ("naive", "laplace", "biharmonic") :return: a :class:`ElasticityDoubleLayerWrapperBase` object """ if nu == 0.5: from pytential.symbolic.stokes import StressletWrapper return StressletWrapper(dim=dim, mu=mu, method=method) - if method == "naive": + if method == Method.naive: return ElasticityDoubleLayerWrapperNaive(dim=dim, mu=mu, nu=nu) - elif method == "biharmonic": + elif method == Method.biharmonic: return ElasticityDoubleLayerWrapperBiharmonic(dim=dim, mu=mu, nu=nu) - elif method == "laplace": + elif method == Method.laplace: if nu == 0.5: from pytential.symbolic.stokes import StressletWrapperTornberg return StressletWrapperTornberg(dim=dim, diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 849dda2ef..d48e785a7 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -33,7 +33,7 @@ ElasticityDoubleLayerWrapperBase, _ElasticityWrapperNaiveOrBiharmonic, _ElasticityDoubleLayerWrapperNaiveOrBiharmonic, - _MU_SYM_DEFAULT) + Method, _MU_SYM_DEFAULT) from pytential.symbolic.typing import ExpressionT from dataclasses import dataclass from functools import cached_property @@ -409,34 +409,42 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, # {{{ StokesletWrapper dispatch method -def StokesletWrapper(dim, mu=_MU_SYM_DEFAULT, method=None): # noqa: N806 +def StokesletWrapper( + dim: int, + mu: ExpressionT = _MU_SYM_DEFAULT, + method: Method = None + ): # noqa: N806 if method is 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": + method = Method.naive + if method == Method.naive: return StokesletWrapperNaive(dim=dim, mu=mu) - elif method == "biharmonic": + elif method == Method.biharmonic: return StokesletWrapperBiharmonic(dim=dim, mu=mu) - elif method == "laplace": + elif method == Method.laplace: return StokesletWrapperTornberg(dim=dim, mu=mu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") -def StressletWrapper(dim, mu=_MU_SYM_DEFAULT, method=None): # noqa: N806 +def StressletWrapper( + dim: int, + mu: ExpressionT = _MU_SYM_DEFAULT, + method: Method = None + ): # noqa: N806 if method is 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": + method = Method.naive + if method == Method.naive: return StressletWrapperNaive(dim=dim, mu=mu) - elif method == "biharmonic": + elif method == Method.biharmonic: return StressletWrapperBiharmonic(dim=dim, mu=mu) - elif method == "laplace": + elif method == Method.laplace: return StressletWrapperTornberg(dim=dim, mu=mu) else: raise ValueError(f"invalid method: {method}." diff --git a/test/test_stokes.py b/test/test_stokes.py index 5a217cf72..57d5b4557 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -29,7 +29,7 @@ from pytential import GeometryCollection, bind, sym from pytential.symbolic.stokes import StokesletWrapper from pytential.symbolic.elasticity import (make_elasticity_wrapper, - make_elasticity_double_layer_wrapper, + make_elasticity_double_layer_wrapper, Method, ElasticityDoubleLayerWrapperBase) from meshmode.discretization import Discretization from meshmode.discretization.poly_element import \ @@ -166,9 +166,9 @@ def run_exterior_stokes(actx_factory, *, sym_nu = SpatialConstant("nu2") stokeslet = make_elasticity_wrapper(ambient_dim, mu=sym_mu, - nu=sym_nu, method=method) + nu=sym_nu, method=Method[method]) stresslet = make_elasticity_double_layer_wrapper(ambient_dim, mu=sym_mu, - nu=sym_nu, method=method) + nu=sym_nu, method=Method[method]) if ambient_dim == 2: from pytential.symbolic.stokes import HsiaoKressExteriorStokesOperator @@ -195,7 +195,7 @@ def run_exterior_stokes(actx_factory, *, # Use the naive method here as biharmonic requires source derivatives # of point_source sym_source_pot = make_elasticity_wrapper(ambient_dim, mu=sym_mu, - nu=sym_nu, method="naive").apply(sym_sigma, qbx_forced_limit=None) + nu=sym_nu, method=Method.naive).apply(sym_sigma, qbx_forced_limit=None) # }}} @@ -683,7 +683,7 @@ def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): pde_class = ElasticityPDE identity = pde_class(dim, make_elasticity_wrapper( - case.ambient_dim, mu=1, nu=nu, method=method)) + case.ambient_dim, mu=1, nu=nu, method=Method[method])) for resolution in resolutions: h_max, errors = run_stokes_identity( @@ -730,7 +730,7 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): pde_class = ElasticityPDE identity = pde_class(dim, make_elasticity_double_layer_wrapper( - case.ambient_dim, mu=1, nu=nu, method=method)) + case.ambient_dim, mu=1, nu=nu, method=Method[method])) from pytools.convergence import EOCRecorder eocs = [EOCRecorder() for _ in range(case.ambient_dim)] From f2a9ad55ab9d565fefe39b8726972e4b2af2b467 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 14:32:07 -0600 Subject: [PATCH 086/156] make linter happy --- 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 d48e785a7..16cc38a7d 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -424,7 +424,7 @@ def StokesletWrapper( elif method == Method.biharmonic: return StokesletWrapperBiharmonic(dim=dim, mu=mu) elif method == Method.laplace: - return StokesletWrapperTornberg(dim=dim, mu=mu) + return StokesletWrapperTornberg(dim=dim, mu=mu, nu=0.5) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -445,7 +445,7 @@ def StressletWrapper( elif method == Method.biharmonic: return StressletWrapperBiharmonic(dim=dim, mu=mu) elif method == Method.laplace: - return StressletWrapperTornberg(dim=dim, mu=mu) + return StressletWrapperTornberg(dim=dim, mu=mu, nu=0.5) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") From f5f508c74883089f158e33bd9a37854bc3b14e40 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 14:36:30 -0600 Subject: [PATCH 087/156] Use laplacian instead of laplace --- pytential/symbolic/elasticity.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index bf56ea89a..9d197f503 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -308,9 +308,10 @@ def kernel_dict(self): s = tuple(sorted([i, j, k])) d[(i, j, k)] = d[s] - # For elasticity (nu != 0.5), we need the LaplaceKernel + # For elasticity (nu != 0.5), we need the laplacian of the + # BiharmonicKernel which is the LaplaceKernel. if self.nu != 0.5: - d["laplace"] = LaplaceKernel(self.dim) + d["laplacian"] = LaplaceKernel(self.dim) return d @@ -327,7 +328,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, "laplacian", "laplacian", "laplacian"] 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]]] From 05b4d2caeebe0257cae3a071f165196fabd00076 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 15:18:17 -0600 Subject: [PATCH 088/156] defaults to a variable named --- pytential/symbolic/elasticity.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 9d197f503..c52f110ee 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -423,9 +423,9 @@ def make_elasticity_wrapper( values. :param: dim: dimension - :param: mu: viscosity symbol, defaults to "mu" - :param: nu: poisson ratio symbol, defaults to "nu" - :param: method: method to use, defaults to naive. + :param: mu: viscosity symbol, defaults to a variable named "mu" + :param: nu: poisson ratio symbol, defaults to a variable named "nu" + :param: method: method to use, defaults to the *Method* enum value naive. :return: a :class:`ElasticityWrapperBase` object """ @@ -459,9 +459,9 @@ def make_elasticity_double_layer_wrapper( input values. :param: dim: dimension - :param: mu: viscosity symbol, defaults to "mu" - :param: nu: poisson ratio symbol, defaults to "nu" - :param: method: method to use, defaults to "naive". + :param: mu: viscosity symbol, defaults to a variable named "mu" + :param: nu: poisson ratio symbol, defaults to a variable named "nu" + :param: method: method to use, defaults to the *Method* enum value naive. :return: a :class:`ElasticityDoubleLayerWrapperBase` object """ From 49c6bd38d9d43f94b18bff8ee6d2170fc33995d2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:10:19 -0600 Subject: [PATCH 089/156] refactor solve with lu --- pytential/symbolic/pde/system_utils.py | 4 +- pytential/utils.py | 80 +++++++++++++++----------- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 53a6037de..6872ceb47 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -34,7 +34,7 @@ from pytential.symbolic.primitives import (NodeCoordinateComponent, hashable_kernel_args, IntG, DEFAULT_SOURCE) from pytential.symbolic.mappers import IdentityMapper -from pytential.utils import chop, lu_with_post_division_callback +from pytential.utils import chop, solve_with_lu import pytential from typing import List, Mapping, Text, Any, Union, Tuple, Optional @@ -394,7 +394,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, const = 0 logger.debug("%s = ", kernel) - sol = lu_with_post_division_callback(L, U, perm, vec, lambda expr: expr.expand()) + sol = solve_with_lu(L, U, perm, vec, lambda expr: expr.expand()) for i, coeff in enumerate(sol): coeff = chop(coeff, tol) if coeff == 0: diff --git a/pytential/utils.py b/pytential/utils.py index fe18b3d6b..a2ed685c8 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -55,12 +55,48 @@ def chop(expr: sym.Basic, tol) -> sym.Basic: return expr.xreplace(replace_dict) -def lu_with_post_division_callback( +def forward_substitution( + L: sym.Matrix, + b: sym.Matrix, + postprocess_division: Callable[[sym.Basic], sym.Basic], + ) -> sym.Matrix: + """Given a lower triangular matrix *L* and a column vector *b*, + solve the system ``Lx = b`` and apply the callable *postprocess_division* + on each expression at the end of division calls. + """ + 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] = postprocess_division(res[i] / L[i, i]) + return res + + +def backward_substitution( + U: sym.Matrix, + b: sym.Matrix, + postprocess_division: Callable[[sym.Basic], sym.Basic], + ) -> sym.Matrix: + """Given an upper triangular matrix *U* and a column vector *b*, + solve the system ``Ux = b`` and apply the callable *postprocess_division* + on each expression at the end of division calls. + """ + 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] = callback(res[i] / U[i, i]) + return res + + +def solve_from_lu( L: sym.Matrix, U: sym.Matrix, - perm: Iterable[sym.Matrix], - b: Iterable[sym.Matrix], - callback: Callable[[sym.Basic], sym.Basic] + perm: Iterable[int], + b: sym.Matrix, + postprocess_division: Callable[[sym.Basic], sym.Basic] ) -> sym.Matrix: """Given an LU factorization and a vector, solve a linear system with intermediate results expanded to avoid @@ -69,34 +105,14 @@ def lu_with_post_division_callback( :param L: lower triangular matrix :param U: upper triangular matrix :param perm: permutation matrix - :param b: column vector to solve for - :param callback: callable that is called after each division + :param b: a column vector to solve for + :param postprocess_division: callable that is called after each division """ - 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] = callback(res[i] / L[i, i]) - 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] = callback(res[i] / U[i, i]) - return res - - def permute_fwd(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, permute_fwd(b, perm))) + # Permute first + res = sym.Matrix(b) + for p, q in perm: + res[p], res[q] = res[q], res[p] + + return backward_substitution(U, forward_substitution(L, res)) # vim: foldmethod=marker From bd98c02caeb025f51590b8fb3991bbcc6783266d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:12:07 -0600 Subject: [PATCH 090/156] type annotate base_kernel --- 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 6872ceb47..fcc753a2f 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -25,7 +25,7 @@ import sumpy.symbolic as sym import pymbolic from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, - ExpressionKernel, KernelWrapper, TargetPointMultiplier, + ExpressionKernel, KernelWrapper, TargetPointMultiplier, Kernel, DirectionalSourceDerivative) from pytools import (memoize_on_first_arg, generate_nonnegative_integer_tuples_summing_to_at_most @@ -60,7 +60,7 @@ def rewrite_using_base_kernel(exprs: List[ExpressionT], - base_kernel=_NO_ARG_SENTINEL) -> List[ExpressionT]: + base_kernel: Kernel = _NO_ARG_SENTINEL) -> List[ExpressionT]: """ Rewrites a list of expressions with :class:`~pytential.symbolic.primitives.IntG` objects using *base_kernel*. Assumes that potentials are smooth, i.e. that From 6d8039b5dfd26a0cc6ed41a4b3517153f3511416 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:14:38 -0600 Subject: [PATCH 091/156] warn if translation variant kernel is found and bail --- pytential/symbolic/pde/system_utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index fcc753a2f..98c6dae56 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -40,6 +40,7 @@ from typing import List, Mapping, Text, Any, Union, Tuple, Optional from pytential.symbolic.typing import ExpressionT +import warnings import logging logger = logging.getLogger(__name__) @@ -156,6 +157,10 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: conv = SympyToPymbolicMapper() knl = int_g.target_kernel + if not knl.is_translation_invariant: + warnings.warn(f"Translation variant kernel ({knl}) found.") + return [int_g] + # we use a symbol for d = (x - y) ds = sympy.symbols(f"d0:{knl.dim}") sources = sympy.symbols(f"y0:{knl.dim}") @@ -174,7 +179,6 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: expr = expr.diff(ds[knl.axis]) found = True else: - import warnings warnings.warn(f"Unknown target kernel ({knl}) found.") return [int_g] knl = knl.inner_kernel From 0536475dbf3fa43eddd92659d1077f5b4d4bfddf Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:21:43 -0600 Subject: [PATCH 092/156] refactor terms --- pytential/symbolic/pde/system_utils.py | 32 ++++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 98c6dae56..39f92d72b 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -194,26 +194,28 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: # u_{d[0], d[1]}(d, y)*d[0]*y[1] + u(d, y) * d[1] # or a single term like u(d, y) * d[1] if isinstance(expr, sympy.Add): - args = expr.args + kernel_terms = expr.args else: - args = [expr] + kernel_terms = [expr] result = [] - for arg in args: - deriv_terms = arg.atoms(sympy.Derivative) - if len(deriv_terms) == 1: - # for eg: we have a term like u_{d[0], d[1]}(d, y) * d[0] * y[1] + for kernel_term in kernel_terms: + deriv_factors = kernel_term.atoms(sympy.Derivative) + if len(deriv_factors) == 1: + # for eg: we ve a term like u_{d[0], d[1]}(d, y) * d[0] * y[1] # deriv_term is u_{d[0], d[1]} - deriv_term = deriv_terms.pop() - # eg: rest_terms is d[0] * y[1] - rest_terms = sympy.Poly(arg.xreplace({deriv_term: 1}), *ds, *sources) + (deriv_factor,) = deriv_factors + # eg: remaining_factors is d[0] * y[1] + remaining_factors = sympy.Poly(kernel_term.xreplace( + {deriv_factor: 1}), *ds, *sources) # eg: derivatives is (d[0], 1), (d[1], 1) - derivatives = deriv_term.args[1:] - elif len(deriv_terms) == 0: + derivatives = deriv_factor.args[1:] + elif len(deriv_factors) == 0: # for eg: we have a term like u(d, y) * d[1] - # rest_terms = d[1] - rest_terms = sympy.Poly(arg.xreplace({orig_expr: 1}), *ds, *sources) - derivatives = [(d, 0) for d in ds] + # remaining_factors = d[1] + remaining_factors = sympy.Poly(kernel_term.xreplace( + {orig_expr: 1}), *ds, *sources) + derivatives = [] else: raise AssertionError("impossible condition") @@ -228,7 +230,7 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: new_source_kernels.append(knl) new_int_g = int_g.copy(source_kernels=new_source_kernels) - (monom, coeff,) = rest_terms.terms()[0] + (monom, coeff,) = remaining_factors.terms()[0] # Now from d[0]*y[1], we separate the two terms # d terms end up in the expression and y terms end up in the density d_terms, y_terms = monom[:len(ds)], monom[len(ds):] From f74a020fbcf8006aa9a9e88bb69f59a3282c5271 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:22:34 -0600 Subject: [PATCH 093/156] fix incomplete refactor --- pytential/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/utils.py b/pytential/utils.py index a2ed685c8..b82daec54 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -87,7 +87,7 @@ def backward_substitution( 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] = callback(res[i] / U[i, i]) + res[i] = postprocess_division(res[i] / U[i, i]) return res From dd79bf967cbdb11f2a88726bdb017e168b503f5b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:23:35 -0600 Subject: [PATCH 094/156] improve commnet --- 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 39f92d72b..213f64efd 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -202,8 +202,8 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: for kernel_term in kernel_terms: deriv_factors = kernel_term.atoms(sympy.Derivative) if len(deriv_factors) == 1: - # for eg: we ve a term like u_{d[0], d[1]}(d, y) * d[0] * y[1] - # deriv_term is u_{d[0], d[1]} + # for eg: if kernel_terms is u_{d[0], d[1]}(d, y) * d[0] * y[1] + # deriv_factor is u_{d[0], d[1]} (deriv_factor,) = deriv_factors # eg: remaining_factors is d[0] * y[1] remaining_factors = sympy.Poly(kernel_term.xreplace( From cb2ca1d6d1ce32ab71610ba7506c061d5fd59737 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 18:27:00 -0600 Subject: [PATCH 095/156] integral -> convolution --- pytential/symbolic/elasticity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index c52f110ee..a05deb4e3 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -230,7 +230,7 @@ def kernel_dict(self): def _get_int_g(self, idx, density_sym, dir_vec_sym, qbx_forced_limit, deriv_dirs): """ - Returns the Integral of the elasticity kernel given by `idx` + Returns the convolution of the elasticity kernel given by `idx` and its derivatives. """ res = _create_int_g(self.kernel_dict[idx], deriv_dirs, @@ -318,8 +318,8 @@ def kernel_dict(self): 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. + Returns the convolution of the double layer of the elasticity kernel + given by `idx` and its derivatives. """ nu = self.nu From 70ad03e8f5f1d4a4b0a04fde1609b98f0016e37b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 19:19:08 -0600 Subject: [PATCH 096/156] fix bad refactor --- 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 213f64efd..827524799 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -34,7 +34,7 @@ from pytential.symbolic.primitives import (NodeCoordinateComponent, hashable_kernel_args, IntG, DEFAULT_SOURCE) from pytential.symbolic.mappers import IdentityMapper -from pytential.utils import chop, solve_with_lu +from pytential.utils import chop, solve_from_lu import pytential from typing import List, Mapping, Text, Any, Union, Tuple, Optional @@ -400,7 +400,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, const = 0 logger.debug("%s = ", kernel) - sol = solve_with_lu(L, U, perm, vec, lambda expr: expr.expand()) + sol = solve_from_lu(L, U, perm, vec, lambda expr: expr.expand()) for i, coeff in enumerate(sol): coeff = chop(coeff, tol) if coeff == 0: From 917dc457ba720face2f021f4183a3eb62a72c858 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 19:34:14 -0600 Subject: [PATCH 097/156] pass through postprocess_division --- pytential/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytential/utils.py b/pytential/utils.py index b82daec54..9010e3baa 100644 --- a/pytential/utils.py +++ b/pytential/utils.py @@ -113,6 +113,10 @@ def solve_from_lu( for p, q in perm: res[p], res[q] = res[q], res[p] - return backward_substitution(U, forward_substitution(L, res)) + return backward_substitution( + U, + forward_substitution(L, res, postprocess_division), + postprocess_division, + ) # vim: foldmethod=marker From 53693d72031e63b670da31d18f2b5d3362c3f6d2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 20:22:19 -0600 Subject: [PATCH 098/156] use a data class for deriv relation --- pytential/symbolic/pde/system_utils.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 827524799..81329dbc3 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -41,6 +41,8 @@ from pytential.symbolic.typing import ExpressionT import warnings +from dataclasses import dataclass + import logging logger = logging.getLogger(__name__) @@ -312,7 +314,7 @@ def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) base_kernel, hashable_kernel_arguments=( hashable_kernel_args(int_g.kernel_arguments))) - const = deriv_relation[0] + const = deriv_relation.const # 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` @@ -343,7 +345,7 @@ def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) new_kernel_args = filter_kernel_arguments([base_kernel], int_g.kernel_arguments) - for mi, c in deriv_relation[1]: + for mi, c in deriv_relation.linear_combination: knl = source_kernel.replace_base_kernel(base_kernel) for d, val in enumerate(mi): for _ in range(val): @@ -354,12 +356,23 @@ def _rewrite_int_g_using_base_kernel(int_g: IntG, base_kernel: ExpressionKernel) return result +@dataclass +class DerivRelation: + """A class to hold the relationship between a kernel and a base kernel. + *linear_combination* is a list of pairs of (mi, coeff). + The relation is given by, + `kernel = const + sum(deriv(base_kernel, mi) * coeff)` + """ + const: ExpressionT + linear_combination: List[Tuple[Tuple[int, ...]], ExpressionT] + + def get_deriv_relation(kernels: List[ExpressionKernel], base_kernel: ExpressionKernel, kernel_arguments: Mapping[Text, Any], tol: float = 1e-10, order: Optional[int] = None) \ - -> List[Tuple[ExpressionT, List[Tuple[Tuple[int], ExpressionT]]]]: + -> List[DerivRelation]: res = [] for knl in kernels: res.append(get_deriv_relation_kernel(knl, base_kernel, @@ -374,14 +387,14 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, hashable_kernel_arguments: Tuple[Tuple[Text, Any], ...], tol: float = 1e-10, order: Optional[int] = None) \ - -> Tuple[ExpressionT, List[Tuple[Tuple[int, ...], ExpressionT]]]: + -> DerivRelation: """Takes a *kernel* and a base_kernel* as input and re-writes the *kernel* as a linear combination of derivatives of *base_kernel* up-to order *order* and a constant. *tol* is an upper limit for small numbers that are replaced with zero in the numerical procedure. :returns: the constant and a list of (multi-index, coeff) to represent the - linear combination of derivatives + linear combination of derivatives as a *DerivRelation* object. """ kernel_arguments = dict(hashable_kernel_arguments) (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, @@ -416,7 +429,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, const = sympy_conv(coeff * _get_sympy_kernel_expression( kernel.global_scaling_const, kernel_arguments)) logger.debug(" + %s", const) - return (const, result) + return DerivRelation(const, result) @memoize_on_first_arg From 88d493814fd982c7c9574b7b0482000f02fe4c88 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 20:29:56 -0600 Subject: [PATCH 099/156] use a dataclass for LUFactorization --- pytential/symbolic/pde/system_utils.py | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 81329dbc3..b410d8ba1 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -397,7 +397,9 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, linear combination of derivatives as a *DerivRelation* object. """ kernel_arguments = dict(hashable_kernel_arguments) - (L, U, perm), rand, mis = _get_base_kernel_matrix(base_kernel, order=order, + (L, U, perm), rand, mis = _get_base_kernel_matrix_lu_factorization( + base_kernel, + order=order, hashable_kernel_arguments=hashable_kernel_arguments) dim = base_kernel.dim sym_vec = sym.make_sym_vector("d", dim) @@ -432,12 +434,26 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, return DerivRelation(const, result) +@dataclass +class LUFactorization: + L: sym.Matrix + U: sym.Matrix + perm: List[Tuple[int, int]] + + @memoize_on_first_arg -def _get_base_kernel_matrix(base_kernel: ExpressionKernel, +def _get_base_kernel_matrix_lu_factorization(base_kernel: ExpressionKernel, hashable_kernel_arguments: Tuple[Tuple[Text, Any], ...], order: Optional[int] = None, retries: int = 3) \ - -> Tuple[Tuple[sym.Matrix, sym.Matrix, List[Tuple[int, int]]], - np.ndarray, List[Tuple[int, ...]]]: + -> Tuple[LUFactorization, np.ndarray, List[Tuple[int, ...]]]: + """ + Takes a *base_kernel* and samples the kernel and its derivatives upto + order *order*. + + :returns: a tuple with the LU factorization of the sampled matrix, + the sampled points, and the multi-indices corresponding to the + derivatives represented by the rows of the matrix. + """ dim = base_kernel.dim pde = base_kernel.get_pde_as_diff_op() @@ -506,14 +522,14 @@ def _get_base_kernel_matrix(base_kernel: ExpressionKernel, raise NotImplementedError("Computing derivative relation when " "the base kernel's derivatives are linearly dependent has not " "been implemented yet.") - return _get_base_kernel_matrix( + return _get_base_kernel_matrix_lu_factorization( base_kernel=base_kernel, hashable_kernel_arguments=hashable_kernel_arguments, order=order, retries=retries-1, ) - return (L, U, perm), rand, mis + return LUFactorization(L, U, perm), rand, mis def evalf(expr, prec=100): From b2befb4df837d0522e75cdaf1713253da319242b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 20:43:33 -0600 Subject: [PATCH 100/156] Fix type annotation --- 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 b410d8ba1..dcdfc69bf 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -364,7 +364,7 @@ class DerivRelation: `kernel = const + sum(deriv(base_kernel, mi) * coeff)` """ const: ExpressionT - linear_combination: List[Tuple[Tuple[int, ...]], ExpressionT] + linear_combination: List[Tuple[Tuple[int, ...], ExpressionT]] def get_deriv_relation(kernels: List[ExpressionKernel], From 5ac3ade93fa3a3d8c4b075f3a44de41e1faeffa6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 21:52:37 -0600 Subject: [PATCH 101/156] fix tests --- pytential/symbolic/elasticity.py | 2 +- pytential/symbolic/pde/system_utils.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index a05deb4e3..5f07b7cdb 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -626,7 +626,7 @@ class ElasticityWrapperYoshida(ElasticityWrapperBase): def __post_init__(self): if not self.dim == 3: raise ValueError("unsupported dimension given to " - "ElasticityDoubleLayerWrapperYoshida: {self.dim}") + f"ElasticityDoubleLayerWrapperYoshida: {self.dim}") @cached_property def stresslet(self): diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index dcdfc69bf..380ae35ea 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -54,6 +54,7 @@ __doc__ = """ .. autofunction:: rewrite_using_base_kernel .. autofunction:: get_deriv_relation +.. autoclass:: DerivRelation """ @@ -397,7 +398,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, linear combination of derivatives as a *DerivRelation* object. """ kernel_arguments = dict(hashable_kernel_arguments) - (L, U, perm), rand, mis = _get_base_kernel_matrix_lu_factorization( + lu, rand, mis = _get_base_kernel_matrix_lu_factorization( base_kernel, order=order, hashable_kernel_arguments=hashable_kernel_arguments) @@ -415,7 +416,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, const = 0 logger.debug("%s = ", kernel) - sol = solve_from_lu(L, U, perm, vec, lambda expr: expr.expand()) + sol = solve_from_lu(lu.L, lu.U, lu.perm, vec, lambda expr: expr.expand()) for i, coeff in enumerate(sol): coeff = chop(coeff, tol) if coeff == 0: From 9a92de36d11be0ce0374f126b8f5918d0c9590d4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 21:53:00 -0600 Subject: [PATCH 102/156] avoid creating IntGs with 0 densities --- pytential/symbolic/stokes.py | 42 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 16cc38a7d..a644249e9 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -304,8 +304,11 @@ def __post_init__(self): def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): stresslet = StressletWrapperTornberg(self.dim, self.mu, self.nu) return stresslet.apply_single_and_double_layer(density_vec_sym, - [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, - extra_deriv_dirs) + [0]*self.dim, [0]*self.dim, + qbx_forced_limit=qbx_forced_limit, + stokeslet_weight=1, + stresslet_weight=0, + extra_deriv_dirs=extra_deriv_dirs) @dataclass @@ -332,7 +335,25 @@ def laplace_kernel(self): def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): return self.apply_single_and_double_layer([0]*self.dim, - density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1, extra_deriv_dirs) + density_vec_sym, dir_vec_sym, + qbx_forced_limit=qbx_forced_limit, + stokeslet_weight=0, + stresslet_weight=1, + extra_deriv_dirs=extra_deriv_dirs) + + def _create_int_g(self, target_kernel, source_kernels, densities, qbx_forced_limit): + new_source_kernels = [] + new_densities = [] + for source_kernel, density in zip(source_kernels, densities): + if density != 0.0: + new_source_kernels.append(source_kernel) + new_densities.append(density) + if not new_densities: + return 0 + return sym.IntG(target_kernel=target_kernel, + source_kernels=tuple(new_source_kernels), + densities=tuple(new_densities), + qbx_forced_limit=qbx_forced_limit) def apply_single_and_double_layer(self, stokeslet_density_vec_sym, stresslet_density_vec_sym, dir_vec_sym, @@ -342,10 +363,6 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, sym_expr = np.zeros((self.dim,), dtype=object) source = sym.nodes(self.dim).as_vector() - common_source_kernels = [ - AxisSourceDerivative(k, self.laplace_kernel) for - k in range(self.dim)] - common_source_kernels.append(self.laplace_kernel) # The paper in [1] ignores the scaling we use Stokeslet/Stresslet # and gives formulae for the kernel expression only @@ -356,6 +373,11 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, stresslet_weight *= 3.0 stokeslet_weight *= -0.5*self.mu**(-1) + common_source_kernels = [ + AxisSourceDerivative(k, self.laplace_kernel) for + k in range(self.dim)] + common_source_kernels.append(self.laplace_kernel) + for i in range(self.dim): for j in range(self.dim): densities = [(stresslet_weight/6.0)*( @@ -367,7 +389,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, AxisTargetDerivative(i, self.laplace_kernel)) for deriv_dir in extra_deriv_dirs: target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) - sym_expr[i] -= sym.IntG(target_kernel=target_kernel, + sym_expr[i] -= self._create_int_g(target_kernel=target_kernel, source_kernels=tuple(common_source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) @@ -378,7 +400,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, target_kernel = AxisTargetDerivative( deriv_dir, target_kernel) - sym_expr[i] += sym.IntG(target_kernel=target_kernel, + sym_expr[i] += self._create_int_g(target_kernel=target_kernel, source_kernels=common_source_kernels, densities=densities, qbx_forced_limit=qbx_forced_limit) @@ -397,7 +419,7 @@ def apply_single_and_double_layer(self, stokeslet_density_vec_sym, target_kernel = AxisTargetDerivative(i, self.laplace_kernel) for deriv_dir in extra_deriv_dirs: target_kernel = AxisTargetDerivative(deriv_dir, target_kernel) - sym_expr[i] += sym.IntG(target_kernel=target_kernel, + sym_expr[i] += self._create_int_g(target_kernel=target_kernel, source_kernels=tuple(common_source_kernels), densities=tuple(densities), qbx_forced_limit=qbx_forced_limit) From bb3424d9d783223eedcfa1044ba0c65e695a1925 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 13 Dec 2022 22:38:55 -0600 Subject: [PATCH 103/156] fix too long line --- pytential/symbolic/stokes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index a644249e9..8850efe1d 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -341,7 +341,8 @@ def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, stresslet_weight=1, extra_deriv_dirs=extra_deriv_dirs) - def _create_int_g(self, target_kernel, source_kernels, densities, qbx_forced_limit): + def _create_int_g(self, target_kernel, source_kernels, densities, + qbx_forced_limit): new_source_kernels = [] new_densities = [] for source_kernel, density in zip(source_kernels, densities): From ec9760c854cdbcd811ba362ee1a32515b7d14ea3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 14 Dec 2022 11:21:26 -0600 Subject: [PATCH 104/156] Try both solutions instead of relying on the symbolic backend --- test/test_pde_system_utils.py | 44 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index ae80975be..6b41a79fb 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -55,22 +55,30 @@ def test_convert_target_point_multiplier(): d = make_sym_vector("d", 3) - if USE_SYMENGINE: - r2 = d[2]**2 + d[1]**2 + d[0]**2 - eknl1 = ExpressionKernel(3, d[1]*d[0]*r2**prim.Quotient(-3, 2), - knl.global_scaling_const, False) - else: - r2 = d[0]**2 + d[1]**2 + d[2]**2 - eknl1 = ExpressionKernel(3, d[0]*d[1]*r2**prim.Quotient(-3, 2), + r2 = d[2]**2 + d[1]**2 + d[0]**2 + eknl0 = ExpressionKernel(3, d[1]*d[0]*r2**prim.Quotient(-3, 2), knl.global_scaling_const, False) eknl2 = ExpressionKernel(3, d[0]*r2**prim.Quotient(-1, 2), knl.global_scaling_const, False) - expected_int_g = IntG(eknl1, [eknl1], [1], qbx_forced_limit=1) + \ + + r2 = d[0]**2 + d[1]**2 + d[2]**2 + eknl1 = ExpressionKernel(3, d[0]*d[1]*r2**prim.Quotient(-3, 2), + knl.global_scaling_const, False) + eknl3 = ExpressionKernel(3, d[0]*r2**prim.Quotient(-1, 2), + knl.global_scaling_const, False) + + possible_int_g1 = IntG(eknl0, [eknl0], [1], qbx_forced_limit=1) + \ IntG(eknl2, [eknl2], [2], qbx_forced_limit=1) + \ IntG(knl, [AxisSourceDerivative(1, knl), knl], [xs[0], 2*xs[0]], qbx_forced_limit=1) - assert expected_int_g == sum(convert_target_transformation_to_source(int_g)) + possible_int_g2 = IntG(eknl1, [eknl1], [1], qbx_forced_limit=1) + \ + IntG(eknl3, [eknl3], [2], qbx_forced_limit=1) + \ + IntG(knl, [AxisSourceDerivative(1, knl), knl], + [xs[0], 2*xs[0]], qbx_forced_limit=1) + + assert sum(convert_target_transformation_to_source(int_g)) in \ + [possible_int_g1, possible_int_g2] def test_product_rule(): @@ -81,16 +89,20 @@ def test_product_rule(): qbx_forced_limit=1) d = make_sym_vector("d", 3) - if USE_SYMENGINE: - r2 = d[2]**2 + d[1]**2 + d[0]**2 - else: - r2 = d[0]**2 + d[1]**2 + d[2]**2 - eknl = ExpressionKernel(3, d[0]**2*r2**prim.Quotient(-3, 2), + r2 = d[2]**2 + d[1]**2 + d[0]**2 + eknl0 = ExpressionKernel(3, d[0]**2*r2**prim.Quotient(-3, 2), + knl.global_scaling_const, False) + r2 = d[0]**2 + d[1]**2 + d[2]**2 + eknl1 = ExpressionKernel(3, d[0]**2*r2**prim.Quotient(-3, 2), knl.global_scaling_const, False) - expected_int_g = IntG(eknl, [eknl], [-1], qbx_forced_limit=1) + \ + + possible_int_g1 = IntG(eknl0, [eknl0], [-1], qbx_forced_limit=1) + \ + IntG(knl, [AxisSourceDerivative(0, knl)], [xs[0]*(-1)], qbx_forced_limit=1) + possible_int_g2 = IntG(eknl1, [eknl1], [-1], qbx_forced_limit=1) + \ IntG(knl, [AxisSourceDerivative(0, knl)], [xs[0]*(-1)], qbx_forced_limit=1) - assert expected_int_g == sum(convert_target_transformation_to_source(int_g)) + assert sum(convert_target_transformation_to_source(int_g)) in \ + [possible_int_g1, possible_int_g2] def test_convert_helmholtz(): From cca85fb68ccef094af1e29e4d6d98c9d8bb24c14 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 14 Dec 2022 11:37:52 -0600 Subject: [PATCH 105/156] reduce diff --- 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 57d5b4557..e415b1478 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -39,13 +39,14 @@ from meshmode import _acf # noqa: F401 from arraycontext import pytest_generate_tests_for_array_contexts +from meshmode.array_context import PytestPyOpenCLArrayContextFactory import extra_int_eq_data as eid import logging logger = logging.getLogger(__name__) pytest_generate_tests = pytest_generate_tests_for_array_contexts([ - "pyopencl-deprecated", + PytestPyOpenCLArrayContextFactory, ]) From 8a46f4b30fa4b85f2fa97e48bc556b2ff077e97f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:07:05 -0600 Subject: [PATCH 106/156] Add a comment about not having spatial constant mapping methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- pytential/symbolic/elasticity.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 5f07b7cdb..a2e1150a3 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -50,6 +50,8 @@ # {{{ ElasiticityWrapper ABCs +# It is OK if these "escape" into pytential expressions because mappers will +# use the MRO to dispatch them to `map_variable`. _MU_SYM_DEFAULT = SpatialConstant("mu") _NU_SYM_DEFAULT = SpatialConstant("nu") From 86a516b352626396cf756b7c93855061fc5ec849 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:08:10 -0600 Subject: [PATCH 107/156] fix bad merge --- pytential/symbolic/mappers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytential/symbolic/mappers.py b/pytential/symbolic/mappers.py index d0e726e71..e4081e0f8 100644 --- a/pytential/symbolic/mappers.py +++ b/pytential/symbolic/mappers.py @@ -290,6 +290,10 @@ def _default_dofdesc(self, dofdesc): dofdesc = dofdesc.copy(geometry=self.default_target) else: dofdesc = dofdesc.copy(geometry=self.default_source) + elif dofdesc.geometry is prim.DEFAULT_SOURCE: + dofdesc = dofdesc.copy(geometry=self.default_source) + elif dofdesc.geometry is prim.DEFAULT_TARGET: + dofdesc = dofdesc.copy(geometry=self.default_target) return dofdesc From 1f6a49d0902ca063e0c644f1da79b597f8dc940d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:11:26 -0600 Subject: [PATCH 108/156] Update warning message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- 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 380ae35ea..887793bf4 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -182,7 +182,8 @@ def convert_target_transformation_to_source(int_g: IntG) -> List[IntG]: expr = expr.diff(ds[knl.axis]) found = True else: - warnings.warn(f"Unknown target kernel ({knl}) found.") + warnings.warn(f"Unknown target kernel ({knl}) found. " + "Returning IntG expression unchanged.") return [int_g] knl = knl.inner_kernel From ae70ec275fe34dbd852b0a96e25e00643d40c0de Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:11:56 -0600 Subject: [PATCH 109/156] explain -1, -1, -1 and 0, 0, 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andreas Klöckner --- 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 887793bf4..16860450a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -468,7 +468,8 @@ def _get_base_kernel_matrix_lu_factorization(base_kernel: ExpressionKernel, "been implemented yet.") mis = sorted(gnitstam(order, dim), key=sum) - # (-1, -1, -1) represent a constant + # (-1, -1, -1) represents a constant + # ((0,0,0) would be "function with no derivatives") mis.append((-1, -1, -1)) if order == pde.order: From 13b46a5bda04790163a4699154a4c1537c1a29e7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:10:31 -0600 Subject: [PATCH 110/156] operator -> wrapper --- test/test_stokes.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index e415b1478..d3eda8bb8 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -580,9 +580,9 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): # {{{ test Stokes PDE class StokesPDE: - def __init__(self, ambient_dim, operator): + def __init__(self, ambient_dim, wrapper): self.ambient_dim = ambient_dim - self.operator = operator + self.wrapper = wrapper def apply_operator(self): dim = self.ambient_dim @@ -590,15 +590,15 @@ def apply_operator(self): "density_vec_sym": [1]*dim, "qbx_forced_limit": 1, } - if isinstance(self.operator, ElasticityDoubleLayerWrapperBase): + if isinstance(self.wrapper, ElasticityDoubleLayerWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() - d_u = [self.operator.apply(**args, extra_deriv_dirs=(i,)) + d_u = [self.wrapper.apply(**args, extra_deriv_dirs=(i,)) for i in range(dim)] - dd_u = [self.operator.apply(**args, extra_deriv_dirs=(i, i)) + dd_u = [self.wrapper.apply(**args, extra_deriv_dirs=(i, i)) for i in range(dim)] laplace_u = [sum(dd_u[j][i] for j in range(dim)) for i in range(dim)] - d_p = [self.operator.apply_pressure(**args, extra_deriv_dirs=(i,)) + d_p = [self.wrapper.apply_pressure(**args, extra_deriv_dirs=(i,)) for i in range(dim)] eqs = [laplace_u[i] - d_p[i] for i in range(dim)] + [sum(d_u)] return make_obj_array(eqs) @@ -608,9 +608,9 @@ def ref_result(self): class ElasticityPDE: - def __init__(self, ambient_dim, operator): + def __init__(self, ambient_dim, wrapper): self.ambient_dim = ambient_dim - self.operator = operator + self.wrapper = wrapper def apply_operator(self): dim = self.ambient_dim @@ -618,11 +618,11 @@ def apply_operator(self): "density_vec_sym": [1]*dim, "qbx_forced_limit": 1, } - if isinstance(self.operator, ElasticityDoubleLayerWrapperBase): + if isinstance(self.wrapper, ElasticityDoubleLayerWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() - mu = self.operator.mu - nu = self.operator.nu + mu = self.wrapper.mu + nu = self.wrapper.nu assert nu != 0.5 lam = 2*nu*mu/(1-2*nu) @@ -630,7 +630,7 @@ def apply_operator(self): for i in range(dim): for j in range(i + 1): - derivs[(i, j)] = self.operator.apply(**args, + derivs[(i, j)] = self.wrapper.apply(**args, extra_deriv_dirs=(i, j)) derivs[(j, i)] = derivs[(i, j)] From a604d071893b9d2597f47e558a8325810d036c22 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:13:00 -0600 Subject: [PATCH 111/156] -1 with size dim --- 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 16860450a..d633b7dd2 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -422,7 +422,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, coeff = chop(coeff, tol) if coeff == 0: continue - if mis[i] != (-1, -1, -1): + if mis[i] != (-1,)*dim: coeff *= _get_sympy_kernel_expression(kernel.global_scaling_const, kernel_arguments) coeff /= _get_sympy_kernel_expression(base_kernel.global_scaling_const, @@ -470,7 +470,7 @@ def _get_base_kernel_matrix_lu_factorization(base_kernel: ExpressionKernel, mis = sorted(gnitstam(order, dim), key=sum) # (-1, -1, -1) represents a constant # ((0,0,0) would be "function with no derivatives") - mis.append((-1, -1, -1)) + mis.append((-1,)*dim) if order == pde.order: pde_mis = [ident.mi for eq in pde.eqs for ident in eq.keys()] From 7190431a6b3788cd51a5086708fabf225b49053f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:17:12 -0600 Subject: [PATCH 112/156] List -> Sequence --- pytential/symbolic/pde/system_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index d633b7dd2..a54165722 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -63,7 +63,7 @@ _NO_ARG_SENTINEL = object() -def rewrite_using_base_kernel(exprs: List[ExpressionT], +def rewrite_using_base_kernel(exprs: Sequence[ExpressionT], base_kernel: Kernel = _NO_ARG_SENTINEL) -> List[ExpressionT]: """ Rewrites a list of expressions with :class:`~pytential.symbolic.primitives.IntG` @@ -130,8 +130,8 @@ def _get_sympy_kernel_expression(expr: ExpressionT, return res -def _monom_to_expr(monom: List[int], - variables: List[Union[sym.Basic, ExpressionT]]) \ +def _monom_to_expr(monom: Sequence[int], + variables: Sequence[Union[sym.Basic, ExpressionT]]) \ -> Union[sym.Basic, ExpressionT]: """Convert a monomial to an expression using given variables. @@ -366,10 +366,10 @@ class DerivRelation: `kernel = const + sum(deriv(base_kernel, mi) * coeff)` """ const: ExpressionT - linear_combination: List[Tuple[Tuple[int, ...], ExpressionT]] + linear_combination: Sequence[Tuple[Tuple[int, ...], ExpressionT]] -def get_deriv_relation(kernels: List[ExpressionKernel], +def get_deriv_relation(kernels: Sequence[ExpressionKernel], base_kernel: ExpressionKernel, kernel_arguments: Mapping[Text, Any], tol: float = 1e-10, @@ -440,7 +440,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, class LUFactorization: L: sym.Matrix U: sym.Matrix - perm: List[Tuple[int, int]] + perm: Sequence[Tuple[int, int]] @memoize_on_first_arg From 80040fa97d423ff073d2e84c4ee480da2500a7b5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:29:41 -0600 Subject: [PATCH 113/156] unit test for solve_from_lu --- test/test_tools.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_tools.py b/test/test_tools.py index ea249d288..0056466b7 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -278,6 +278,23 @@ def test_add_geometry_to_collection(actx_factory): # }}} +# {{{ test solve_from_lu + +def test_solve_from_lu(): + import sumpy.symbolic as sym + from pytential.utils import solve_from_lu + x, y, z = sym.symbols("x, y, z") + m = sym.Matrix([[0, x, y], [1, 0, x], [y, 2, 5]]) + L, U, perm = m.LUdecomposition() + + b = sym.Matrix([z, 1, 2]) + sol = solve_from_lu(L, U, perm, b, lambda x: x.expand()) + expected = m.solve(b) + + assert (sol - expected).expand() == sym.Matrix([0, 0, 0]) + + +# }}} # You can test individual routines by typing # $ python test_tools.py 'test_routine()' From 2b591c8c90dfb286e304040e0ce47407ec5b15d3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 21:47:26 -0600 Subject: [PATCH 114/156] import Sequence --- 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 a54165722..ace7a42bc 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -37,7 +37,7 @@ from pytential.utils import chop, solve_from_lu import pytential -from typing import List, Mapping, Text, Any, Union, Tuple, Optional +from typing import List, Mapping, Text, Any, Union, Tuple, Optional, Sequence from pytential.symbolic.typing import ExpressionT import warnings From ee467cfab665957c3aa6cd376c5d09c2b2879c2c Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 23:11:50 -0600 Subject: [PATCH 115/156] Fix applying extra_deriv_dirs --- pytential/symbolic/elasticity.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index a2e1150a3..f7f632245 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -550,12 +550,12 @@ def add_extra_deriv_dirs(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))) + res = -int_g.copy(target_kernel=add_extra_deriv_dirs( + TargetPointMultiplier(j, + AxisTargetDerivative(i, int_g.target_kernel)))) if i == j: - res += (3 - 4*nu)*int_g + res += (3 - 4*nu)*int_g.copy( + target_kernel=add_extra_deriv_dirs(int_g.target_kernel)) return res / (4*mu*(1 - nu)) def Q(i, int_g): From 2c32cc33970cd93711352b5f2ce1dc02e858ab40 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 23:12:51 -0600 Subject: [PATCH 116/156] add a test for errors --- test/test_stokes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_stokes.py b/test/test_stokes.py index d3eda8bb8..ef11910fd 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -692,6 +692,8 @@ def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): resolution=resolution, visualize=visualize) + assert np.all(np.abs(errors) < 1e-11) + @pytest.mark.parametrize("dim, method, nu", [ pytest.param(2, "laplace", 0.5), From 50df2a8a16e21beb2e31b94f157f6f6958a307b7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 23:14:38 -0600 Subject: [PATCH 117/156] formatting: 2 blank lines --- test/test_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_tools.py b/test/test_tools.py index 0056466b7..f4f068bb4 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -276,6 +276,7 @@ def test_add_geometry_to_collection(actx_factory): _add_geometry_to_collection(actx, places, sources) _add_geometry_to_collection(actx, places, targets) + # }}} # {{{ test solve_from_lu From 201822f2e350289cc5b918c62c51c31e29eb864b Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 19 Dec 2022 23:19:03 -0600 Subject: [PATCH 118/156] simplify matrix --- test/test_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_tools.py b/test/test_tools.py index f4f068bb4..b2b1d1555 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -292,7 +292,8 @@ def test_solve_from_lu(): sol = solve_from_lu(L, U, perm, b, lambda x: x.expand()) expected = m.solve(b) - assert (sol - expected).expand() == sym.Matrix([0, 0, 0]) + assert (sol - expected).expand().applyfunc(lambda x: x.simplify()) \ + == sym.Matrix([0, 0, 0]) # }}} From 61349e680ac6954199e4e73af0883e46146699bb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sat, 24 Dec 2022 18:41:07 -0600 Subject: [PATCH 119/156] Check using calculus patch as well --- test/test_stokes.py | 210 ++++++++++++++++++++++++++++---------------- 1 file changed, 135 insertions(+), 75 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index ef11910fd..662381266 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -579,6 +579,45 @@ def test_stresslet_identity(actx_factory, cls, visualize=False): # {{{ test Stokes PDE +def run_stokes_pde(actx_factory, case, identity, resolution, visualize=False): + from sumpy.point_calculus import CalculusPatch + from pytential.target import PointsTarget + actx = actx_factory() + + dim = case.ambient_dim + qbx = case.get_layer_potential(actx, resolution, case.target_order) + + h_min = actx.to_numpy(bind(qbx, sym.h_min(dim))(actx)) + h_max = actx.to_numpy(bind(qbx, sym.h_max(dim))(actx)) + + if dim == 2: + h = h_max + else: + h = h_max / 5 + + cp = CalculusPatch([0, 0, 0][:dim], h=h, order=case.target_order + 1) + targets = PointsTarget(actx.freeze(actx.from_numpy(cp.points))) + + places = GeometryCollection({case.name: qbx, "cp": targets}, + auto_where=(case.name, "cp")) + + potential = bind(places, identity.apply_operator())(actx) + potential_host = actx.to_numpy(potential) + result = identity.apply_pde_using_calculus_patch(cp, potential_host) + result_pytential = actx.to_numpy(bind(places, + identity.apply_pde_using_pytential())(actx)) + + m = np.max([np.linalg.norm(p, ord=np.inf) for p in potential_host]) + error = [np.linalg.norm(x, ord=np.inf)/m for x in result] + + error += [np.linalg.norm(x, ord=np.inf)/m for x in result_pytential] + logger.info("resolution %4d h_min %.5e h_max %.5e error " + + ("%.5e " * places.ambient_dim), + resolution, h_min, h_max, *error) + + return h_max, error + + class StokesPDE: def __init__(self, ambient_dim, wrapper): self.ambient_dim = ambient_dim @@ -588,7 +627,20 @@ def apply_operator(self): dim = self.ambient_dim args = { "density_vec_sym": [1]*dim, - "qbx_forced_limit": 1, + "qbx_forced_limit": None, + } + if isinstance(self.wrapper, ElasticityDoubleLayerWrapperBase): + args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() + + velocity = self.wrapper.apply(**args) + pressure = self.wrapper.apply_pressure(**args) + return make_obj_array([*velocity, pressure]) + + def apply_pde_using_pytential(self): + dim = self.ambient_dim + args = { + "density_vec_sym": [1]*dim, + "qbx_forced_limit": None, } if isinstance(self.wrapper, ElasticityDoubleLayerWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() @@ -600,11 +652,17 @@ def apply_operator(self): laplace_u = [sum(dd_u[j][i] for j in range(dim)) for i in range(dim)] d_p = [self.wrapper.apply_pressure(**args, extra_deriv_dirs=(i,)) for i in range(dim)] - eqs = [laplace_u[i] - d_p[i] for i in range(dim)] + [sum(d_u)] + eqs = [laplace_u[i] - d_p[i] for i in range(dim)] + \ + [sum(d_u[i][i] for i in range(dim))] return make_obj_array(eqs) - def ref_result(self): - return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) + def apply_pde_using_calculus_patch(self, cp, potential): + dim = self.ambient_dim + velocity = potential[:dim] + pressure = potential[dim] + eqs = [cp.laplace(velocity[i]) - cp.diff(i, pressure) for i in range(dim)] \ + + [sum(cp.diff(i, velocity[i]) for i in range(dim))] + return make_obj_array(eqs) class ElasticityPDE: @@ -616,7 +674,18 @@ def apply_operator(self): dim = self.ambient_dim args = { "density_vec_sym": [1]*dim, - "qbx_forced_limit": 1, + "qbx_forced_limit": None, + } + if isinstance(self.wrapper, ElasticityDoubleLayerWrapperBase): + args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() + + return self.wrapper.apply(**args) + + def apply_pde_using_pytential(self): + dim = self.ambient_dim + args = { + "density_vec_sym": [1]*dim, + "qbx_forced_limit": None, } if isinstance(self.wrapper, ElasticityDoubleLayerWrapperBase): args["dir_vec_sym"] = sym.normal(self.ambient_dim).as_vector() @@ -642,82 +711,64 @@ def apply_operator(self): eqs = [(lam + mu)*grad_of_div_u[i] + mu*laplace_u[i] for i in range(dim)] return make_obj_array(eqs) - def ref_result(self): - return make_obj_array([1.0e-15 * sym.Ones()] * self.ambient_dim) - - -@pytest.mark.parametrize("dim, method, nu", [ - pytest.param(2, "biharmonic", 0.4), - pytest.param(2, "biharmonic", 0.5), - pytest.param(2, "laplace", 0.5), - pytest.param(3, "laplace", 0.5), - pytest.param(3, "laplace", 0.4), - pytest.param(2, "naive", 0.4, marks=pytest.mark.slowtest), - pytest.param(3, "naive", 0.4, marks=pytest.mark.slowtest), - pytest.param(2, "naive", 0.5, marks=pytest.mark.slowtest), - pytest.param(3, "naive", 0.5, marks=pytest.mark.slowtest), - # FIXME: re-enable when merge_int_g_exprs is in - pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.skip), - pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.skip), - # FIXME: re-enable when StokesletWrapperYoshida is implemented for 2D - pytest.param(2, "laplace", 0.4, marks=pytest.mark.xfail), - ]) -def test_stokeslet_pde(actx_factory, dim, method, nu, visualize=False): - if visualize: - logging.basicConfig(level=logging.INFO) - - if dim == 2: - case_cls = eid.StarfishTestCase - resolutions = [16, 32, 64, 96, 128] - else: - case_cls = eid.SpheroidTestCase - resolutions = [0, 1, 2] - - source_ovsmp = 4 if dim == 2 else 8 - case = case_cls(fmm_backend=None, - target_order=5, qbx_order=3, source_ovsmp=source_ovsmp, - resolutions=resolutions) - - if nu == 0.5: - pde_class = StokesPDE - else: - pde_class = ElasticityPDE - - identity = pde_class(dim, make_elasticity_wrapper( - case.ambient_dim, mu=1, nu=nu, method=Method[method])) + def apply_pde_using_calculus_patch(self, cp, potential): + dim = self.ambient_dim + mu = self.wrapper.mu + nu = self.wrapper.nu + assert nu != 0.5 + lam = 2*nu*mu/(1-2*nu) - for resolution in resolutions: - h_max, errors = run_stokes_identity( - actx_factory, case, identity, - resolution=resolution, - visualize=visualize) + laplace_u = [cp.laplace(potential[i]) for i in range(dim)] + grad_of_div_u = [ + sum(cp.diff(j, cp.diff(i, potential[j])) for j in range(dim)) + for i in range(dim)] - assert np.all(np.abs(errors) < 1e-11) + # Navier-Cauchy equations + eqs = [(lam + mu)*grad_of_div_u[i] + mu*laplace_u[i] for i in range(dim)] + return make_obj_array(eqs) -@pytest.mark.parametrize("dim, method, nu", [ - pytest.param(2, "laplace", 0.5), - pytest.param(3, "laplace", 0.5), - pytest.param(3, "laplace", 0.4), - pytest.param(2, "naive", 0.4, marks=pytest.mark.slowtest), - pytest.param(3, "naive", 0.4, marks=pytest.mark.slowtest), - pytest.param(2, "naive", 0.5, marks=pytest.mark.slowtest), - pytest.param(3, "naive", 0.5, marks=pytest.mark.slowtest), +@pytest.mark.parametrize("dim, method, nu, double_layer", [ + # Single layer + pytest.param(2, "biharmonic", 0.4, False), + pytest.param(2, "biharmonic", 0.5, False), + pytest.param(2, "laplace", 0.5, False), + pytest.param(3, "laplace", 0.5, False), + pytest.param(3, "laplace", 0.4, False), + pytest.param(2, "naive", 0.4, False, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.4, False, marks=pytest.mark.slowtest), + pytest.param(2, "naive", 0.5, False, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.5, False, marks=pytest.mark.slowtest), # FIXME: re-enable when merge_int_g_exprs is in - pytest.param(2, "biharmonic", 0.4, marks=pytest.mark.skip), - pytest.param(2, "biharmonic", 0.5, marks=pytest.mark.skip), - pytest.param(3, "biharmonic", 0.4, marks=pytest.mark.skip), - pytest.param(3, "biharmonic", 0.5, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.4, False, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.5, False, marks=pytest.mark.skip), + # FIXME: re-enable when StokesletWrapperYoshida is implemented for 2D + pytest.param(2, "laplace", 0.4, False, marks=pytest.mark.xfail), + + # Double layer + pytest.param(2, "laplace", 0.5, True), + pytest.param(3, "laplace", 0.5, True), + pytest.param(3, "laplace", 0.4, True), + pytest.param(2, "naive", 0.4, True, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.4, True, marks=pytest.mark.slowtest), + pytest.param(2, "naive", 0.5, True, marks=pytest.mark.slowtest), + pytest.param(3, "naive", 0.5, True, marks=pytest.mark.slowtest), + # FIXME: re-enable when merge_int_g_exprs is in + pytest.param(2, "biharmonic", 0.4, True, marks=pytest.mark.skip), + pytest.param(2, "biharmonic", 0.5, True, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.4, True, marks=pytest.mark.skip), + pytest.param(3, "biharmonic", 0.5, True, marks=pytest.mark.skip), # FIXME: re-enable when StressletWrapperYoshida is implemented for 2D - pytest.param(2, "laplace", 0.4, marks=pytest.mark.xfail), + pytest.param(2, "laplace", 0.4, True, marks=pytest.mark.xfail), ]) -def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): +def test_elasticity_pde(actx_factory, dim, method, nu, is_double_layer, + visualize=False): if visualize: logging.basicConfig(level=logging.INFO) if dim == 2: - case_cls = eid.StarfishTestCase - resolutions = [16, 32, 64, 96, 128] + case_cls = eid.CircleTestCase + resolutions = [4, 8, 16] else: case_cls = eid.SpheroidTestCase resolutions = [0, 1, 2] @@ -732,14 +783,18 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): else: pde_class = ElasticityPDE - identity = pde_class(dim, make_elasticity_double_layer_wrapper( - case.ambient_dim, mu=1, nu=nu, method=Method[method])) + if is_double_layer: + identity = pde_class(dim, make_elasticity_double_layer_wrapper( + case.ambient_dim, mu=1, nu=nu, method=Method[method])) + else: + identity = pde_class(dim, make_elasticity_wrapper( + case.ambient_dim, mu=1, nu=nu, method=Method[method])) from pytools.convergence import EOCRecorder - eocs = [EOCRecorder() for _ in range(case.ambient_dim)] + eocs = [EOCRecorder() for _ in range(2*case.ambient_dim)] for resolution in case.resolutions: - h_max, errors = run_stokes_identity( + h_max, errors = run_stokes_pde( actx_factory, case, identity, resolution=resolution, visualize=visualize) @@ -755,7 +810,12 @@ def test_stresslet_pde(actx_factory, dim, method, nu, visualize=False): for eoc in eocs: order = min(case.target_order, case.qbx_order) - assert eoc.order_estimate() > order - 1.5 + # Sometimes the error has already converged to a value close to + # machine epsilon. In that case we have to reduce the resolution + # to increase the error to observe the error behaviour, but when + # reducing the resolution is not possible, we check that all the + # errors are below a certain threshold. + assert eoc.order_estimate() > order - 1 or eoc.max_error() < 1e-10 # }}} From 684ec6628f55f0f4afb7d06eacadd52f7d2c0904 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 25 Dec 2022 21:02:12 -0600 Subject: [PATCH 120/156] describe the math --- pytential/symbolic/pde/system_utils.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index ace7a42bc..9ba2dfb17 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -375,6 +375,20 @@ def get_deriv_relation(kernels: Sequence[ExpressionKernel], tol: float = 1e-10, order: Optional[int] = None) \ -> List[DerivRelation]: + """ + Given a sequence of *kernels*, a *base_kernel* and an *order*, this + gives a relation between the *base_kernel* and each of the *kernels*. + For each kernel in *kernels* we have that the kernel is equal to the + linear combination of derivatives of *base_kernel* up to the order + *order* and a constant. i.e., + + kernel = \sum_{m \in M(order)} \partial^m baseKernel \partial x^m + + const. + + When *order* is not given, the algorithm starts with one and increases + the order upto the order of the PDE satisfied by the *base_kernel* until + a relation is found. + """ res = [] for knl in kernels: res.append(get_deriv_relation_kernel(knl, base_kernel, From 10e5221e7b394ae3ce30fb6f589c83f084692bc4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 25 Dec 2022 21:20:05 -0600 Subject: [PATCH 121/156] mention column of ones in A --- pytential/symbolic/pde/system_utils.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 9ba2dfb17..5b8c62751 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -385,6 +385,13 @@ def get_deriv_relation(kernels: Sequence[ExpressionKernel], kernel = \sum_{m \in M(order)} \partial^m baseKernel \partial x^m + const. + This is done by sampling the baseKernel and its derivatives at random + points to get a matrix ``A``, then sampling the kernel at the same + points to get a matrix ``b`` and solving for the system ``Ax = b`` using + an LU factorization of ``A``. The solution ``x`` is the vector of weights + in the linear combination. To represent a constant in the relation we + add a column of ones into ``A``. + When *order* is not given, the algorithm starts with one and increases the order upto the order of the PDE satisfied by the *base_kernel* until a relation is found. From 79783f8280e0e59bb6b7b92520e46ad9365e6ac3 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 27 Dec 2022 00:06:44 -0600 Subject: [PATCH 122/156] fix typo --- pytential/symbolic/pde/system_utils.py | 2 +- test/test_stokes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index 5b8c62751..f2d2d757c 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -375,7 +375,7 @@ def get_deriv_relation(kernels: Sequence[ExpressionKernel], tol: float = 1e-10, order: Optional[int] = None) \ -> List[DerivRelation]: - """ + r""" Given a sequence of *kernels*, a *base_kernel* and an *order*, this gives a relation between the *base_kernel* and each of the *kernels*. For each kernel in *kernels* we have that the kernel is equal to the diff --git a/test/test_stokes.py b/test/test_stokes.py index 662381266..3cec608ad 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -728,7 +728,7 @@ def apply_pde_using_calculus_patch(self, cp, potential): return make_obj_array(eqs) -@pytest.mark.parametrize("dim, method, nu, double_layer", [ +@pytest.mark.parametrize("dim, method, nu, is_double_layer", [ # Single layer pytest.param(2, "biharmonic", 0.4, False), pytest.param(2, "biharmonic", 0.5, False), From 7d8ef25201febd152b3a9f3cd5091113b787d295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Thu, 5 Jan 2023 18:40:31 -0600 Subject: [PATCH 123/156] Fix misleading naming in test_stokes --- test/test_stokes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_stokes.py b/test/test_stokes.py index 3cec608ad..6f0cfc4e6 100644 --- a/test/test_stokes.py +++ b/test/test_stokes.py @@ -365,17 +365,17 @@ def test_exterior_stokes(actx_factory, ambient_dim, method, nu, visualize=False) error_format="%.8e", eoc_format="%.2f")) - extra_order = 0 + orders_lost = 0 if method == "biharmonic": - extra_order += 1 + orders_lost += 1 elif nu != 0.5: - extra_order += 0.5 + orders_lost += 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 - extra_order + assert eoc.order_estimate() > order - 0.5 - orders_lost # }}} From 781804946bdb05d74097e7153ff10d3a7cc35a05 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 11 Jan 2023 19:17:21 -0600 Subject: [PATCH 124/156] Placate flake8-comprehensions --- pytential/symbolic/elasticity.py | 4 ++-- pytential/symbolic/pde/system_utils.py | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index f7f632245..0c8647c76 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -186,8 +186,8 @@ def _create_int_g(knl, deriv_dirs, density, **kwargs): for deriv_dir in deriv_dirs: knl = AxisTargetDerivative(deriv_dir, knl) - kernel_arg_names = set(karg.loopy_arg.name - for karg in (knl.get_args() + knl.get_source_args())) + kernel_arg_names = {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 diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/system_utils.py index f2d2d757c..18c02c719 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -431,8 +431,8 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, expr = _get_sympy_kernel_expression(kernel.expression, kernel_arguments) 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.append(evalf(expr.xreplace + ({k: v for k, v in zip(sym_vec, rand[:, i])}))) vec = sym.Matrix(vec) result = [] const = 0 @@ -518,9 +518,7 @@ def _get_base_kernel_matrix_lu_factorization(base_kernel: ExpressionKernel, 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]) - ) + replace_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) From 69931e71cd76c3e93d799890be097e1cccde42eb Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 14:21:05 -0500 Subject: [PATCH 125/156] Placate flake8-comprehensions again --- 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 18c02c719..9a15e7c3a 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -431,8 +431,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, expr = _get_sympy_kernel_expression(kernel.expression, kernel_arguments) vec = [] for i in range(len(mis)): - vec.append(evalf(expr.xreplace - ({k: v for k, v in zip(sym_vec, rand[:, i])}))) + vec.append(evalf(expr.xreplace(dict(sym_vec, rand[:, i])))) vec = sym.Matrix(vec) result = [] const = 0 @@ -518,7 +517,7 @@ def _get_base_kernel_matrix_lu_factorization(base_kernel: ExpressionKernel, if nderivs == 0: continue expr = expr.diff(sym_vec[var_idx], nderivs) - replace_dict = {k: v for k, v in zip(sym_vec, rand[:, rand_vec_idx])} + replace_dict = dict(zip(sym_vec, rand[:, rand_vec_idx])) eval_expr = evalf(expr.xreplace(replace_dict)) row.append(eval_expr) row.append(1) From 180a4645b30f1c0771487306eda7a71b5bccadf7 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 15:19:42 -0500 Subject: [PATCH 126/156] fix typo --- 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 9a15e7c3a..b50361c0e 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/system_utils.py @@ -431,7 +431,7 @@ def get_deriv_relation_kernel(kernel: ExpressionKernel, expr = _get_sympy_kernel_expression(kernel.expression, kernel_arguments) vec = [] for i in range(len(mis)): - vec.append(evalf(expr.xreplace(dict(sym_vec, rand[:, i])))) + vec.append(evalf(expr.xreplace(dict(zip(sym_vec, rand[:, i]))))) vec = sym.Matrix(vec) result = [] const = 0 From 4136c6a4afb44ff1df073f3cb41940c43d9c885f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 15:02:55 -0500 Subject: [PATCH 127/156] add code to reduce and merge fmms --- doc/symbolic.rst | 6 +- pytential/symbolic/elasticity.py | 2 +- pytential/symbolic/pde/systems/__init__.py | 31 ++ .../pde/{system_utils.py => systems/deriv.py} | 1 + pytential/symbolic/pde/systems/merge.py | 521 ++++++++++++++++++ pytential/symbolic/pde/systems/reduce.py | 497 +++++++++++++++++ pytential/symbolic/stokes.py | 2 +- test/test_pde_system_utils.py | 2 +- 8 files changed, 1056 insertions(+), 6 deletions(-) create mode 100644 pytential/symbolic/pde/systems/__init__.py rename pytential/symbolic/pde/{system_utils.py => systems/deriv.py} (99%) create mode 100644 pytential/symbolic/pde/systems/merge.py create mode 100644 pytential/symbolic/pde/systems/reduce.py diff --git a/doc/symbolic.rst b/doc/symbolic.rst index f5b9f24b3..3f368d76b 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -57,7 +57,7 @@ Internals Rewriting expressions with ``IntG``\ s ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automodule:: pytential.symbolic.pde.system_utils +.. automodule:: pytential.symbolic.pde.systems.merge Internal affairs ---------------- @@ -77,6 +77,6 @@ How a symbolic operator gets executed Rewriting expressions with ``IntG``\ s internals ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automethod:: pytential.symbolic.pde.system_utils.convert_target_transformation_to_source -.. automethod:: pytential.symbolic.pde.system_utils.rewrite_int_g_using_base_kernel +.. automethod:: pytential.symbolic.pde.systems.merge.convert_target_transformation_to_source +.. automethod:: pytential.symbolic.pde.systems.merge.rewrite_int_g_using_base_kernel diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index 0c8647c76..c7db3db6f 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -26,7 +26,7 @@ import numpy as np from pytential import sym -from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel +from pytential.symbolic.pde.systems import rewrite_using_base_kernel from sumpy.kernel import (StressletKernel, LaplaceKernel, StokesletKernel, ElasticityKernel, BiharmonicKernel, Kernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) diff --git a/pytential/symbolic/pde/systems/__init__.py b/pytential/symbolic/pde/systems/__init__.py new file mode 100644 index 000000000..4f972cca1 --- /dev/null +++ b/pytential/symbolic/pde/systems/__init__.py @@ -0,0 +1,31 @@ +__copyright__ = "Copyright (C) 2023 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 .deriv import rewrite_using_base_kernel, get_deriv_relation +from .merge import merge_int_g_exprs + +__all__ = ( + "rewrite_using_base_kernel", + "get_deriv_relation", + "merge_int_g_exprs", + "reduce_number_of_fmms", + ) diff --git a/pytential/symbolic/pde/system_utils.py b/pytential/symbolic/pde/systems/deriv.py similarity index 99% rename from pytential/symbolic/pde/system_utils.py rename to pytential/symbolic/pde/systems/deriv.py index b50361c0e..d28eca855 100644 --- a/pytential/symbolic/pde/system_utils.py +++ b/pytential/symbolic/pde/systems/deriv.py @@ -579,6 +579,7 @@ def filter_kernel_arguments(knls, kernel_arguments): return {k: v for (k, v) in kernel_arguments.items() if k in kernel_arg_names} + # }}} diff --git a/pytential/symbolic/pde/systems/merge.py b/pytential/symbolic/pde/systems/merge.py new file mode 100644 index 000000000..8f346e9fc --- /dev/null +++ b/pytential/symbolic/pde/systems/merge.py @@ -0,0 +1,521 @@ +__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 pymbolic.mapper.coefficient import CoefficientCollector +from pymbolic.geometric_algebra.mapper import WalkMapper +from pymbolic.mapper import CombineMapper + +from sumpy.kernel import (AxisTargetDerivative, AxisSourceDerivative, + KernelWrapper, TargetPointMultiplier, + DirectionalSourceDerivative, DirectionalDerivative) + +from pytential.symbolic.primitives import ( + hashable_kernel_args, hashable_kernel_arg_value) +from pytential.symbolic.mappers import IdentityMapper +from .reduce import reduce_number_of_fmms + +from collections import defaultdict + +import logging +logger = logging.getLogger(__name__) + +__all__ = ( + "merge_int_g_exprs", +) + +__doc__ = """ +.. autofunction:: merge_int_g_exprs +""" + + +# {{{ merge_int_g_exprs + +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 + * :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. + * 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 is done by the + call to :func:`pytential.symbolic.pde.systems.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 + expression of same type with the kernel replaced by base_kernel and its + 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. + """ + # Using a dictionary instead of a set because sets are unordered + all_source_group_identifiers = {} + + result = np.array([0 for _ in exprs], dtype=object) + + 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) + + 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. + logger.debug("%s is not linear", expr) + result[i] += expr + add_int_gs_in_expr(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 DirectionalSourceDerivative to AxisSourceDerivative + # as kernel arguments need to be the same for merging + 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) + 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) + target_group_identifier = get_int_g_target_group_identifier(int_g) + group = (source_group_identifier, target_group_identifier, coeff) + + all_source_group_identifiers[source_group_identifier] = 1 + + if group not in int_gs_by_group: + 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 + + # Do some simplifications after merging. Not stricty necessary + 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 + # 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 = int_g.copy( + densities=simplify_densities(int_g.densities)) + result[i] += result_int_g * coeff + add_int_gs_in_expr(result_int_g) + + # No IntGs found + if all(not int_gs for int_gs in int_gs_by_source_group): + return exprs + + # Do the calculation for each source_group_identifier separately + # and assemble them + replacements = {} + 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). + # 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 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] + + 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 + + +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 get_hashable_kernel_argument(arg): + if hasattr(arg, "__iter__"): + try: + return tuple(arg) + except TypeError: + pass + return arg + + +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 + + +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. + """ + target_arg_names = get_normal_vector_names(int_g.target_kernel) + args = {k: v for k, v in sorted( + int_g.kernel_arguments.items()) if k not in target_arg_names} + return (int_g.source, hashable_kernel_args(args), + int_g.target_kernel.get_base_kernel()) + + +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. + """ + target_arg_names = get_normal_vector_names(int_g.target_kernel) + args = {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, + hashable_kernel_args(args)) + + +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_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, + 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), kernel_arguments=kernel_arguments) + + +def _convert_directional_source_knl_to_axis_source(knl, knl_arguments): + if isinstance(knl, DirectionalSourceDerivative): + dim = knl.dim + dir_vec = knl_arguments[knl.dir_vec_name] + + res = [] + inner_result = _convert_directional_source_knl_to_axis_source( + 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_arguments) + return [(knl.replace_inner_kernel(inner_knl), coeff) for + inner_knl, coeff in inner_result] + else: + return [(knl, 1)] + + +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 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)) + + +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 + + +def is_expr_target_dependent(expr): + mapper = IsExprTargetDependent() + return mapper(expr) + + +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: + 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: + res[k] = v + return + + +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, + ) + + +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() + to_pymbolic = SympyToPymbolicMapper() + result = [] + for density in 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) + + +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(), + kernel_arguments=kernel_arguments) + + +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)) + + +class GetIntGs(WalkMapper): + """A Mapper that walks expressions and collects + :class:`~pytential.symbolic.primitives.IntG` objects + """ + def __init__(self): + self.int_g_s = set() + + def map_int_g(self, expr): + self.int_g_s.add(expr) + + def map_constant(self, expr): + pass + + map_variable = map_constant + handle_unsupported_expression = map_constant + + +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 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), + kernel_arguments=orig_int_g.kernel_arguments) + for int_g in int_gs} + + substitutor = IntGSubstitutor(replacements) + return substitutor(expr) + + +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) + +# }}} diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py new file mode 100644 index 000000000..3e4fe97df --- /dev/null +++ b/pytential/symbolic/pde/systems/reduce.py @@ -0,0 +1,497 @@ +__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, + KernelWrapper) + +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 +from collections import defaultdict + +import logging +logger = logging.getLogger(__name__) + + +__all__ = ( + "reduce_number_of_fmms", + ) + +__doc__ = """ +.. autofunction:: reduce_number_of_fmms +""" + + +# {{{ 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 + :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, 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 + represents a transformation at the source. + + If the expressions given are not linear, then the input expressions are + returned as is. + + :arg int_gs: list of ``IntG`` objects. + + :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. + + 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 "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 share all the + # properties except for the densities, source transformations and target + # transformations. + assert _check_int_gs_common(int_gs) + + 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, + axis_vars) + except ValueError: + logger.debug("could not create matrix from %s", int_gs) + return int_gs + + mat = sympy.Matrix(mat) + + # Factor the matrix into two + try: + 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, + # + # - matrix: R^{m x n}, + # - LHS: R^{m x k}, + # - RHS: R^{k x n}. + # + # If k is greater than or equal to n we are gaining nothing. + # Return as is. + if right_factor.shape[0] >= mat.shape[0]: + return int_gs + + 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( + 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. + source_int_gs_merged = [] + for i in range(right_factor.shape[0]): + source_kernels = [] + densities = [] + for j in range(right_factor.shape[1]): + new_densities = [density * source_exprs[j] for density in + source_int_gs[i][j].densities] + source_kernels.extend(source_int_gs[i][j].source_kernels) + densities.extend(new_densities) + 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]*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].as_poly(*axis_vars, domain=sympy.EX), + int_gs[i], source_int_gs_merged[j]) + + 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_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 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() + 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 + +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. + """ + from pytential.symbolic.pde.systems.merge 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.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 + + +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 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) + to_sympy = PymbolicToSympyMapper() + matrix = [] + + 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 = _kernel_source_derivs_as_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 + + +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}. + + 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 + + def __call__(self, expr): + if expr in self.source_dependent_variables: + 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_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: + 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: + # 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: + 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 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): + d_num = self.rec(expr.numerator) + d_den = self.rec(expr.denominator) + if len(d_den) > 1: + raise ValueError + den_var, den_coeff = list(d_den.items())[0] + + 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) + 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, = d_exponent.values() + if exp == 1: + return d_base + if len(d_base) > 1: + raise NotImplementedError("powers are not implemented") + (var, coeff), = d_base.items() + return {var**exp: coeff**exp} + + 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_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 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. + """ + from sympy.polys.orderings import grevlex + + def _convert_to_matrix(module, *generators): + result = [] + for syzygy in module: + row = [] + for dmp in syzygy.data: + 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(*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] + + intersection = functools.reduce(lambda x, y: x.intersect(y), + column_syzygy_modules) + + # _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): + """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. 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. + 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. + """ + 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_groebner_basis_mat(syzygy_of_mat.T, axis_vars).T + + +def _factor_right(mat, factor_left): + """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) + +# }}} + + +# {{{ 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.systems.merge 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 diff --git a/pytential/symbolic/stokes.py b/pytential/symbolic/stokes.py index 8850efe1d..78aa0f16e 100644 --- a/pytential/symbolic/stokes.py +++ b/pytential/symbolic/stokes.py @@ -26,7 +26,7 @@ import numpy as np from pytential import sym -from pytential.symbolic.pde.system_utils import rewrite_using_base_kernel +from pytential.symbolic.pde.systems import rewrite_using_base_kernel from sumpy.kernel import (LaplaceKernel, BiharmonicKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from pytential.symbolic.elasticity import (ElasticityWrapperBase, diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 6b41a79fb..661bdd39a 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -18,7 +18,7 @@ THE SOFTWARE. """ -from pytential.symbolic.pde.system_utils import ( +from pytential.symbolic.pde.systems.merge import ( convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) from pytential.symbolic.primitives import IntG from pytential import sym From 7fe98ae9e2684f333414543a1a08ac934bd22ab2 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 17:47:36 -0500 Subject: [PATCH 128/156] introduce hashable_kernel_arg_value --- pytential/symbolic/primitives.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 869a4e7f7..6bba928a0 100644 --- a/pytential/symbolic/primitives.py +++ b/pytential/symbolic/primitives.py @@ -1315,14 +1315,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): From 02e56b7e06bcb2494556491eeb6876cb632e7438 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 17:48:37 -0500 Subject: [PATCH 129/156] fold duplicates in source kernels --- pytential/symbolic/primitives.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pytential/symbolic/primitives.py b/pytential/symbolic/primitives.py index 6bba928a0..a771a0d70 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 partial +from collections import OrderedDict import numpy as np @@ -1408,6 +1409,18 @@ 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()) + source_kernels = tuple(knl_density_dict.keys()) + source_kernels = tuple(source_kernels) densities = tuple(densities) kernel_arg_names = set() From 3ef322a80383fee106110b807326307b3cd4fa00 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 18:47:39 -0500 Subject: [PATCH 130/156] fix doctests --- doc/symbolic.rst | 7 ++--- pytential/symbolic/pde/systems/merge.py | 36 +++++++++++++------------ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/doc/symbolic.rst b/doc/symbolic.rst index 3f368d76b..19d7cc65a 100644 --- a/doc/symbolic.rst +++ b/doc/symbolic.rst @@ -58,6 +58,8 @@ Rewriting expressions with ``IntG``\ s ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: pytential.symbolic.pde.systems.merge +.. automodule:: pytential.symbolic.pde.systems.reduce +.. automodule:: pytential.symbolic.pde.systems.deriv Internal affairs ---------------- @@ -77,6 +79,5 @@ How a symbolic operator gets executed Rewriting expressions with ``IntG``\ s internals ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automethod:: pytential.symbolic.pde.systems.merge.convert_target_transformation_to_source -.. automethod:: pytential.symbolic.pde.systems.merge.rewrite_int_g_using_base_kernel - +.. automethod:: pytential.symbolic.pde.systems.deriv.convert_target_transformation_to_source +.. automethod:: pytential.symbolic.pde.systems.deriv.rewrite_int_g_using_base_kernel diff --git a/pytential/symbolic/pde/systems/merge.py b/pytential/symbolic/pde/systems/merge.py index 8f346e9fc..fa6e00a57 100644 --- a/pytential/symbolic/pde/systems/merge.py +++ b/pytential/symbolic/pde/systems/merge.py @@ -55,23 +55,25 @@ 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 - * :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. - * 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 is done by the - call to :func:`pytential.symbolic.pde.systems.reduce_number_of_fmms`. + Several techniques are used for merging and reducing number of FMMs: + + * :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. + * 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 is done by the + call to :func:`pytential.symbolic.pde.systems.reduce.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 expression of same type with the kernel replaced by base_kernel and its From e3959cd36ccbec0b765abecb07b1c39621111d46 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 23 Mar 2023 18:47:57 -0500 Subject: [PATCH 131/156] Fix op_group_features --- pytential/unregularized.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytential/unregularized.py b/pytential/unregularized.py index d18d188a6..d19a0e223 100644 --- a/pytential/unregularized.py +++ b/pytential/unregularized.py @@ -119,10 +119,11 @@ 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), - expr.target_kernel, + TargetTransformationRemover()(expr.target_kernel), ) return result From 61be7641dde23da519a865b808f8731f31d8ca09 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 24 Mar 2023 15:36:01 -0500 Subject: [PATCH 132/156] add unit tests --- pytential/symbolic/pde/systems/merge.py | 2 +- test/test_pde_system_utils.py | 288 +++++++++++++++++++++++- 2 files changed, 284 insertions(+), 6 deletions(-) diff --git a/pytential/symbolic/pde/systems/merge.py b/pytential/symbolic/pde/systems/merge.py index fa6e00a57..110013307 100644 --- a/pytential/symbolic/pde/systems/merge.py +++ b/pytential/symbolic/pde/systems/merge.py @@ -404,7 +404,7 @@ def merge_kernel_arguments(x, y): f"values were {res[k]} and {v}") else: res[k] = v - return + return res 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 661bdd39a..ab5773d62 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -18,20 +18,24 @@ THE SOFTWARE. """ -from pytential.symbolic.pde.systems.merge import ( - convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) -from pytential.symbolic.primitives import IntG +from pytential.symbolic.primitives import (IntG, int_g_vec, D, + NodeCoordinateComponent) from pytential import sym import pytential +from pytential.symbolic.pde.systems import (merge_int_g_exprs, + rewrite_using_base_kernel) +from pytential.symbolic.pde.systems.deriv import ( + convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) + import numpy as np from sumpy.kernel import ( LaplaceKernel, HelmholtzKernel, ExpressionKernel, BiharmonicKernel, - StokesletKernel, + StokesletKernel, DirectionalTargetDerivative, AxisTargetDerivative, TargetPointMultiplier, AxisSourceDerivative) from sumpy.symbolic import USE_SYMENGINE -from pymbolic.primitives import make_sym_vector +from pymbolic.primitives import make_sym_vector, Variable import pymbolic.primitives as prim @@ -175,3 +179,277 @@ def test_convert_int_g_base_with_const_and_deriv(): qbx_forced_limit=1) assert rewrite_int_g_using_base_kernel(int_g, base_kernel=base_knl) == expected_int_g + + +def test_reduce_number_of_fmms(): + dim = 3 + knl = LaplaceKernel(dim) + densities = make_sym_vector("sigma", 2) + mu = Variable("mu") + + int_g1 = \ + 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) + + 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") + nu = Variable("nu") + + int_g1 = \ + int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(0, knl)), + mu * nu * densities[0], qbx_forced_limit=1) + \ + int_g_vec(AxisSourceDerivative(1, AxisSourceDerivative(1, knl)), + mu * nu * 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, nu]) + + # Merging reduces 4 FMMs to 2 FMMs. No further reduction of FMMs. + int_g3 = \ + IntG(target_kernel=knl, + 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(1, knl)), + AxisSourceDerivative(2, AxisSourceDerivative(0, knl))], + densities=[densities[1], densities[0]], + 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) + + 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)] + + source_kernels = list(reversed([ + 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 _ 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 _ in range(dim)] + [2*density], + qbx_forced_limit=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 + + +def test_merge_different_qbx_forced_limit(): + 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_g1.copy(target_kernel=AxisTargetDerivative(0, laplace_knl)) + + 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) + + 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_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) + 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 + + +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") + + 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] + 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=[density]) + assert result[0] == int_g3 + + result = merge_int_g_exprs([int_g1 + int_g2]) + 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 + 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 + + +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_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 From 4f351c428c040281886857de03c63d60acb089ec Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 24 Mar 2023 15:43:27 -0500 Subject: [PATCH 133/156] support timing_data in BoundExpression --- 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 7c642d8ce..99aa1862b 100644 --- a/pytential/symbolic/execution.py +++ b/pytential/symbolic/execution.py @@ -873,7 +873,9 @@ def __call__(self, *args, **kwargs): raise TypeError("More than one positional argument supplied. " "None or an PyOpenCLArrayContext 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 7e4a95d01ac959c43681b434776fba74d6adf2d6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Fri, 24 Mar 2023 15:43:43 -0500 Subject: [PATCH 134/156] merge exprs by default in bind --- pytential/symbolic/execution.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/execution.py b/pytential/symbolic/execution.py index 99aa1862b..d47b0f25f 100644 --- a/pytential/symbolic/execution.py +++ b/pytential/symbolic/execution.py @@ -878,7 +878,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.collection.GeometryCollection`. Alternatively, any list or mapping that is a valid argument for its @@ -900,6 +900,19 @@ def bind(places, expr, auto_where=None): auto_where = places.auto_where expr = _prepare_expr(places, expr, auto_where=auto_where) + + if _merge_exprs: + from pytential.symbolic.pde.systems 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 02b3e552af7234730d6c69bc083c9436671e06dc Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 13:48:58 -0500 Subject: [PATCH 135/156] simplify right factor --- pytential/symbolic/pde/systems/__init__.py | 1 + pytential/symbolic/pde/systems/reduce.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/systems/__init__.py b/pytential/symbolic/pde/systems/__init__.py index 4f972cca1..a3239e697 100644 --- a/pytential/symbolic/pde/systems/__init__.py +++ b/pytential/symbolic/pde/systems/__init__.py @@ -22,6 +22,7 @@ from .deriv import rewrite_using_base_kernel, get_deriv_relation from .merge import merge_int_g_exprs +from .reduce import reduce_number_of_fmms __all__ = ( "rewrite_using_base_kernel", diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 3e4fe97df..c6cb5a00f 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -440,8 +440,9 @@ def _factor_right(mat, factor_left): as they should result in exact polynomial divisions and will have no remainders. """ - return factor_left.LUsolve(sympy.Matrix(mat), + mat = factor_left.LUsolve(sympy.Matrix(mat), iszerofunc=lambda x: x.simplify() == 0) + return mat.applyfunc(lambda x: x.simplify()) # }}} From 8521560362e7901009eafd711cec937a63db8d64 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 14:25:20 -0500 Subject: [PATCH 136/156] don't fail if our method gives a bad answer --- pytential/symbolic/pde/systems/merge.py | 7 +++---- pytential/symbolic/pde/systems/reduce.py | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/pde/systems/merge.py b/pytential/symbolic/pde/systems/merge.py index 110013307..0f8ec84d8 100644 --- a/pytential/symbolic/pde/systems/merge.py +++ b/pytential/symbolic/pde/systems/merge.py @@ -138,7 +138,7 @@ def add_int_gs_in_expr(expr): 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) + new_int_g = merge_sum_of_two_int_gs(int_g, prev_int_g) int_gs_by_group[group] = new_int_g # Do some simplifications after merging. Not stricty necessary @@ -212,8 +212,7 @@ def add_int_gs_in_expr(expr): 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 exprs return result @@ -407,7 +406,7 @@ def merge_kernel_arguments(x, y): return res -def merge_two_int_gs(int_g_1, int_g_2): +def merge_sum_of_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 diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index c6cb5a00f..489b98666 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -199,11 +199,16 @@ def _check_int_gs_common(int_gs): common_int_g = int_gs[0].copy(target_kernel=base_kernel, source_kernels=(base_kernel,), densities=(1,)) + base_target_kernel = int_gs[0].target_kernel + 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 int_g.target_kernel != base_target_kernel: + return False + if common_int_g.qbx_forced_limit != int_g.qbx_forced_limit: return False From 30c958beeb3a798221dd80b0881cae16340d321a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 16:03:04 -0500 Subject: [PATCH 137/156] Add MindlinOperator --- pytential/symbolic/elasticity.py | 264 ++++++++++++++++++++++++++++++- 1 file changed, 260 insertions(+), 4 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index c7db3db6f..b40c7ec5b 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -26,12 +26,14 @@ import numpy as np from pytential import sym -from pytential.symbolic.pde.systems import rewrite_using_base_kernel +from pytential.symbolic.pde.systems import (rewrite_using_base_kernel, + merge_int_g_exprs) +from pytential.symbolic.typing import ExpressionT from sumpy.kernel import (StressletKernel, LaplaceKernel, StokesletKernel, - ElasticityKernel, BiharmonicKernel, Kernel, + ElasticityKernel, BiharmonicKernel, Kernel, LineOfCompressionKernel, AxisTargetDerivative, AxisSourceDerivative, TargetPointMultiplier) from sumpy.symbolic import SpatialConstant -from pytential.symbolic.typing import ExpressionT +import pymbolic from abc import ABC, abstractmethod from dataclasses import dataclass @@ -566,7 +568,7 @@ def Q(i, int_g): sym_expr = np.zeros((3,), dtype=object) kernel = self.laplace_kernel - source = [sym.NodeCoordinateComponent(d) for d in range(3)] + source = sym.nodes(3).as_vector() normal = dir_vec_sym sigma = stresslet_density_vec_sym @@ -640,3 +642,257 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): extra_deriv_dirs) # }}} + + +# {{{ Kelvin operator + +class ElasticityOperator: + """ + .. automethod:: __init__ + .. automethod:: get_density_var + .. automethod:: operator + """ + + def __init__( + self, + dim: int, + mu: ExpressionT = _MU_SYM_DEFAULT, + nu: ExpressionT = _NU_SYM_DEFAULT, + method: Method = Method.naive): + + self.dim = dim + self.double_layer_op = make_elasticity_double_layer_wrapper( + dim=dim, mu=mu, nu=nu, method=method) + self.single_layer_op = make_elasticity_wrapper( + dim=dim, mu=mu, nu=nu, method=method) + self.mu = mu + self.nu = nu + + def get_density_var(self, name="sigma"): + """ + :returns: a (potentially) modified right-hand side *b* that matches + requirements of the representation. + """ + return sym.make_sym_vector(name, 3) + + @abstractmethod + def operator(self, sigma): + """ + :returns: the integral operator that should be solved to obtain the + density *sigma*. + """ + raise NotImplementedError + + +class KelvinOperator(ElasticityOperator): + """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, + mu: ExpressionT = _MU_SYM_DEFAULT, + nu: ExpressionT = _NU_SYM_DEFAULT, + method: Method = Method.naive) -> ElasticityWrapperBase: + + super().__init__(dim=3, method=method, + mu=mu, nu=nu) + self.laplace_kernel = LaplaceKernel(3) + + def operator(self, sigma, *, normal, qbx_forced_limit="avg"): + return self.double_layer_op.apply(sigma, normal, + qbx_forced_limit=qbx_forced_limit) + +# }}} + + +# {{{ Mindlin operator + +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: Method = Method.biharmonic, + mu: ExpressionT = _MU_SYM_DEFAULT, + nu: ExpressionT = _NU_SYM_DEFAULT, + line_of_compression_tol: float = 0.0): + + if method not in [Method.biharmonic, Method.Laplace]: + raise ValueError(f"invalid method: {method}." + "Needs to be one of laplace, biharmonic") + + self.free_space_op = KelvinOperator(method=method, mu=mu, + nu=nu) + self.modified_free_space_op = KelvinOperator( + method=method, mu=mu + 4*nu, nu=-nu) + self.compression_knl = LineOfCompressionKernel(3, 2, mu, nu, + tol=line_of_compression_tol) + + def K(self, sigma, normal, qbx_forced_limit): + return merge_int_g_exprs(self.free_space_op.double_layer_op.apply( + sigma, normal, qbx_forced_limit=qbx_forced_limit)) + + def A(self, sigma, normal, qbx_forced_limit): + result = -self.modified_free_space_op.doube_layer_op.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 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] = pymbolic.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 = sym.nodes(3).as_vector() + 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 == 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.double_layer_op.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 d9801cfed5f9cfc5735f6be038b26d612c28bbac Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 18:18:12 -0500 Subject: [PATCH 138/156] add test for mindlin --- pytential/symbolic/elasticity.py | 111 ++++++++++++++++++------------- test/test_pde_system_utils.py | 21 ++++++ 2 files changed, 85 insertions(+), 47 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index b40c7ec5b..e608b23c9 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -661,19 +661,16 @@ def __init__( method: Method = Method.naive): self.dim = dim - self.double_layer_op = make_elasticity_double_layer_wrapper( - dim=dim, mu=mu, nu=nu, method=method) - self.single_layer_op = make_elasticity_wrapper( - dim=dim, mu=mu, nu=nu, method=method) self.mu = mu self.nu = nu + self.method = method def get_density_var(self, name="sigma"): """ :returns: a (potentially) modified right-hand side *b* that matches requirements of the representation. """ - return sym.make_sym_vector(name, 3) + return sym.make_sym_vector(name, self.dim) @abstractmethod def operator(self, sigma): @@ -700,8 +697,12 @@ def __init__( nu: ExpressionT = _NU_SYM_DEFAULT, method: Method = Method.naive) -> ElasticityWrapperBase: - super().__init__(dim=3, method=method, - mu=mu, nu=nu) + dim = 3 + super().__init__(dim=dim, method=method, mu=mu, nu=nu) + self.double_layer_op = make_elasticity_double_layer_wrapper( + dim=dim, mu=mu, nu=nu, method=method) + self.single_layer_op = make_elasticity_wrapper( + dim=dim, mu=mu, nu=nu, method=method) self.laplace_kernel = LaplaceKernel(3) def operator(self, sigma, *, normal, qbx_forced_limit="avg"): @@ -713,7 +714,7 @@ def operator(self, sigma, *, normal, qbx_forced_limit="avg"): # {{{ Mindlin operator -class MindlinOperator: +class MindlinOperator(ElasticityOperator): """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 @@ -733,16 +734,12 @@ def __init__(self, *, nu: ExpressionT = _NU_SYM_DEFAULT, line_of_compression_tol: float = 0.0): - if method not in [Method.biharmonic, Method.Laplace]: - raise ValueError(f"invalid method: {method}." - "Needs to be one of laplace, biharmonic") - + super().__init__(dim=3, method=method, mu=mu, nu=nu) self.free_space_op = KelvinOperator(method=method, mu=mu, nu=nu) self.modified_free_space_op = KelvinOperator( method=method, mu=mu + 4*nu, nu=-nu) - self.compression_knl = LineOfCompressionKernel(3, 2, mu, nu, - tol=line_of_compression_tol) + self.compression_knl = LineOfCompressionKernel(3, 2, mu, nu) def K(self, sigma, normal, qbx_forced_limit): return merge_int_g_exprs(self.free_space_op.double_layer_op.apply( @@ -793,8 +790,15 @@ def B(self, sigma, normal, qbx_forced_limit): **kwargs) for i in range(3): - sym_expr[i] = int_g.copy(target_kernel=AxisTargetDerivative( - i, int_g.target_kernel)) + if self.method == Method.naive: + source_kernels = [AxisSourceDerivative(i, knl) for knl in + int_g.source_kernels] + densities = [(-1)*density for density in int_g.densities] + sym_expr[i] = int_g.copy(source_kernels=tuple(source_kernels), + densities=tuple(densities)) + else: + sym_expr[i] = int_g.copy(target_kernel=AxisTargetDerivative( + i, int_g.target_kernel)) return sym_expr @@ -808,23 +812,39 @@ def C(self, sigma, normal, qbx_forced_limit): sigma_normal_product = sum(a*b for a, b in zip(sigma, normal)) laplace_kernel = self.free_space_op.laplace_kernel - densities = [] - source_kernels = [] + + # 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) # phi_c in Gimbutas et, al. - densities.extend([ + densities = [ -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([ + source_kernels = [ AxisSourceDerivative(a, AxisSourceDerivative(b, laplace_kernel)) for a, b in source_kernel_dirs - ]) + ] # G in Gimbutas et, al. densities.extend([ @@ -841,31 +861,28 @@ def C(self, sigma, normal, qbx_forced_limit): 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 + if self.method == Method.naive: + source_kernels = [AxisSourceDerivative(i, knl) for knl in + int_g.source_kernels] + densities = [(-1)*density for density in int_g.densities] + + if i == 2: + # Target derivative w.r.t x[2] is flipped due to target image + densities[2] *= -1 + # Subtract H + source_kernels = source_kernels + list(H.source_kernels) + densities = densities + [(-1)*d for d in H.densities] + + result[i] = int_g.copy(source_kernels=tuple(source_kernels), + densities=tuple(densities)) + else: + 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[2] *= -1 + # Subtract H + result[2] -= H return result diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index ab5773d62..ccfe3e9a0 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -26,6 +26,8 @@ rewrite_using_base_kernel) from pytential.symbolic.pde.systems.deriv import ( convert_target_transformation_to_source, rewrite_int_g_using_base_kernel) +from pytential.symbolic.pde.systems.merge import get_number_of_fmms +from pytential.symbolic.elasticity import MindlinOperator, Method import numpy as np @@ -453,3 +455,22 @@ def test_int_gs_in_densities(): qbx_forced_limit=1) assert result[0] == int_g3 + + +def test_mindlin(): + sigma = make_sym_vector("sigma", 3) + normal = make_sym_vector("normal", 3) + + mindlin_op = MindlinOperator(method=Method.naive) + + c = mindlin_op.C(sigma=sigma, normal=normal, qbx_forced_limit=1) + assert get_number_of_fmms(c) == 3 + + c_opt = merge_int_g_exprs(c) + assert get_number_of_fmms(c_opt) == 2 + + b = mindlin_op.B(sigma=sigma, normal=normal, qbx_forced_limit=1) + assert get_number_of_fmms(b) == 3 + + b_opt = merge_int_g_exprs(b) + assert get_number_of_fmms(b_opt) == 1 From 6cc31240af60847f3e843233d4323eead5e92742 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 19:23:28 -0500 Subject: [PATCH 139/156] test that rewriting using biharmonic still gives 2 FMMs for C-image --- test/test_pde_system_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index ccfe3e9a0..426bfe38b 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -469,6 +469,10 @@ def test_mindlin(): c_opt = merge_int_g_exprs(c) assert get_number_of_fmms(c_opt) == 2 + c_opt_biharmonic = merge_int_g_exprs(rewrite_using_base_kernel( + c, base_kernel=BiharmonicKernel(3))) + assert get_number_of_fmms(c_opt_biharmonic) == 2 + b = mindlin_op.B(sigma=sigma, normal=normal, qbx_forced_limit=1) assert get_number_of_fmms(b) == 3 From 33049fd10f06654b5726e6d448b2c60f04c835b1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 23:45:04 -0500 Subject: [PATCH 140/156] Don't simplify unnecessarily --- pytential/symbolic/pde/systems/reduce.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 489b98666..ac2b37b8a 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -121,7 +121,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, + as_poly(expr, axis_vars), 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 @@ -150,6 +150,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): return res +def as_poly(expr, axis_vars): + res = expr.as_poly(*axis_vars, domain=sympy.EX) + if res is None: + return expr.simplify().as_poly(*axis_vars, domain=sympy.EX) + else: + return res + + class GatherAllSourceDependentVariables(WalkMapper): def __init__(self): self.vars = {} From 62230aa56026c9303b71ba2c6dbd2f8723a17c34 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Sun, 26 Mar 2023 23:54:42 -0500 Subject: [PATCH 141/156] fix test failures --- pytential/symbolic/elasticity.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index e608b23c9..c5615dc44 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -746,7 +746,7 @@ def K(self, sigma, normal, qbx_forced_limit): sigma, normal, qbx_forced_limit=qbx_forced_limit)) def A(self, sigma, normal, qbx_forced_limit): - result = -self.modified_free_space_op.doube_layer_op.apply( + result = -self.modified_free_space_op.double_layer_op.apply( sigma, normal, qbx_forced_limit=qbx_forced_limit) new_density = sum(a*b for a, b in zip(sigma, normal)) @@ -899,9 +899,10 @@ 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 = merge_int_g_exprs(resultA + resultC, + result = rewrite_using_base_kernel(resultA + resultC, base_kernel=self.free_space_op.double_layer_op.base_kernel) - result += merge_int_g_exprs(resultB, base_kernel=self.compression_knl) + result += rewrite_using_base_kernel(resultB, + base_kernel=self.compression_knl) return result else: return resultA + resultB + resultC From 61a8b5e4bffd138b225c50425cfb8b41bfe7a4b1 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 28 Mar 2023 18:06:50 -0500 Subject: [PATCH 142/156] verbose pytest --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5391bb397..b6bac7361 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: "Main Script" run: | export SUMPY_FORCE_SYMBOLIC_BACKEND=sympy - export PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-"-m 'not slowtest'"} + export PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-"-m 'not slowtest' -vv"} curl -L -O https://tiker.net/ci-support-v0 . ci-support-v0 @@ -84,7 +84,7 @@ jobs: export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export CONDA_ENVIRONMENT=.test-conda-env.yml - export PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-"-m 'not slowtest'"} + export PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-"-m 'not slowtest' -vv"} curl -L -O https://tiker.net/ci-support-v0 . ci-support-v0 @@ -99,7 +99,7 @@ jobs: - name: "Main Script" run: | export SUMPY_FORCE_SYMBOLIC_BACKEND=symengine - export PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-"-m 'not slowtest'"} + export PYTEST_ADDOPTS=${PYTEST_ADDOPTS:-"-m 'not slowtest' -vv"} curl -L -O https://tiker.net/ci-support-v0 . ci-support-v0 From 4f8e460dde8f86f60a53058fb8556f12556aeede Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 30 Mar 2023 13:17:51 -0500 Subject: [PATCH 143/156] nsimplify before groebner --- pytential/symbolic/pde/systems/reduce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index ac2b37b8a..719c0e45f 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -94,7 +94,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): logger.debug("could not create matrix from %s", int_gs) return int_gs - mat = sympy.Matrix(mat) + mat = sympy.nsimplify(sympy.Matrix(mat)) # Factor the matrix into two try: From 0716f3129550a663cbc8276cb70177bb85b0875a Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Mon, 3 Apr 2023 20:36:16 -0500 Subject: [PATCH 144/156] Use the PDE --- pytential/symbolic/pde/systems/reduce.py | 60 ++++++++++++++++++------ test/test_pde_system_utils.py | 13 +++++ 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 719c0e45f..15247baf6 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -30,6 +30,7 @@ import sympy import functools from collections import defaultdict +from math import prod import logging logger = logging.getLogger(__name__) @@ -96,10 +97,25 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): mat = sympy.nsimplify(sympy.Matrix(mat)) + # Create the quotient ring of polynomials + poly_ring = sympy.EX.old_poly_ring(*axis_vars, order=sympy.grevlex) + try: + pde = int_gs[0].target_kernel.get_base_kernel().get_pde_as_diff_op() + if len(pde.eqs) > 1: + ring = poly_ring + else: + eq = list(pde.eqs)[0] + sym_pde = sum(coeff * prod( + axis_vars[i]**ident.mi[i] for i in range(dim)) + for ident, coeff in eq.items()) + ring = poly_ring / [sym_pde] + except NotImplementedError: + ring = poly_ring + # Factor the matrix into two try: - left_factor = _factor_left(mat, axis_vars) - right_factor = _factor_right(mat, left_factor) + left_factor = factor_left(mat, axis_vars, ring) + right_factor = factor_right(mat, left_factor) except ValueError: logger.debug("could not find a factorization for %s", mat) return int_gs @@ -391,7 +407,26 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): # {{{ factor the matrix -def _syzygy_module_groebner_basis_mat(m, generators): +def _module_intersection(m1, m2): + # Copyright: SymPy developers + # License: BSD-3-Clause + # See: [A Singular Introduction to Commutative Algebra, section 2.8.2] + fi = m1.gens + hi = m2.gens + r = m1.rank + ci = [[0]*(2*r) for _ in range(r)] + for k in range(r): + ci[k][k] = 1 + ci[k][r + k] = 1 + di = [list(f) + [0]*r for f in fi] + ei = [[0]*r + list(h) for h in hi] + syz = m1.ring.free_module(2*r).submodule(*(ci + di + ei))._syzygies() + # FIXME: need to use a minimal generating set. We only discard zeroes here. + nonzero = [x[:r] for x in syz if any(y != m1.ring.zero for y in x[:r])] + return m1.container.submodule(*([-y for y in x[:r]] for x in nonzero)) + + +def syzygy_module_groebner_basis_mat(m, generators, ring): """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 @@ -407,25 +442,22 @@ def _convert_to_matrix(module, *generators): for syzygy in module: row = [] for dmp in syzygy.data: - row.append(sympy.Poly(dmp.to_dict(), *generators, + row.append(sympy.Poly(dmp.data.to_dict(), *generators, domain=sympy.EX).as_expr()) result.append(row) return sympy.Matrix(result) - ring = sympy.EX.old_poly_ring(*generators, order=grevlex) - column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist(), order=grevlex) + column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist()) for i in range(m.shape[1])] column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] - intersection = functools.reduce(lambda x, y: x.intersect(y), + intersection = functools.reduce(_module_intersection, column_syzygy_modules) - # _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) + return _convert_to_matrix(intersection.gens, *generators) -def _factor_left(mat, axis_vars): +def factor_left(mat, axis_vars, ring): """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. The polynomials represent @@ -439,13 +471,13 @@ def _factor_left(mat, axis_vars): 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. """ - syzygy_of_mat = _syzygy_module_groebner_basis_mat(mat, axis_vars) + syzygy_of_mat = syzygy_module_groebner_basis_mat(mat, axis_vars, ring) if len(syzygy_of_mat) == 0: raise ValueError("could not find a factorization") - return _syzygy_module_groebner_basis_mat(syzygy_of_mat.T, axis_vars).T + return syzygy_module_groebner_basis_mat(syzygy_of_mat.T, axis_vars, ring).T -def _factor_right(mat, factor_left): +def factor_right(mat, factor_left): """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 diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 426bfe38b..49bd0e99e 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -478,3 +478,16 @@ def test_mindlin(): b_opt = merge_int_g_exprs(b) assert get_number_of_fmms(b_opt) == 1 + + +def test_paper_reduce_example(): + from sympy import symbols, Matrix, EX + from pytential.symbolic.pde.systems.reduce import \ + syzygy_module_groebner_basis_mat + y = y1, y2, y3 = symbols("y1, y2, y3") + m = Matrix([[y1 * y2, -2*y1**2-2*y3**2], [y1*y3, 2*y2*y3]]) + poly_ring = EX.old_poly_ring(*y) + ring = poly_ring / [y1**2 + y2**2 + y3**2] + fm = syzygy_module_groebner_basis_mat(m, y, ring) + lhs = syzygy_module_groebner_basis_mat(fm.T, y, ring).T + assert lhs == Matrix([[y2], [y3]]) From 79132c1d4fa6ba24b9bec3cba5dbaefe0dbd2895 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 4 Apr 2023 17:36:52 -0500 Subject: [PATCH 145/156] use a minimal genearting set --- pytential/symbolic/pde/systems/reduce.py | 83 +++++++++++++----------- test/test_pde_system_utils.py | 6 +- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 15247baf6..cf7f46255 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -109,13 +109,13 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): axis_vars[i]**ident.mi[i] for i in range(dim)) for ident, coeff in eq.items()) ring = poly_ring / [sym_pde] + # ring = poly_ring except NotImplementedError: ring = poly_ring # Factor the matrix into two try: - left_factor = factor_left(mat, axis_vars, ring) - right_factor = factor_right(mat, left_factor) + left_factor, right_factor = factor(mat, axis_vars, ring) except ValueError: logger.debug("could not find a factorization for %s", mat) return int_gs @@ -407,6 +407,19 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): # {{{ factor the matrix +def minimal_generating_set(m): + gens = list(m.gens) + nonzero = [x for x in gens if any(y != m.ring.zero for y in x)] + basis = nonzero[:] + for x in nonzero: + others = basis[:] + others.remove(x) + if x in m.container.submodule(*others): + basis = others + res = m.container.submodule(*basis) + return res + + def _module_intersection(m1, m2): # Copyright: SymPy developers # License: BSD-3-Clause @@ -421,12 +434,28 @@ def _module_intersection(m1, m2): di = [list(f) + [0]*r for f in fi] ei = [[0]*r + list(h) for h in hi] syz = m1.ring.free_module(2*r).submodule(*(ci + di + ei))._syzygies() - # FIXME: need to use a minimal generating set. We only discard zeroes here. - nonzero = [x[:r] for x in syz if any(y != m1.ring.zero for y in x[:r])] - return m1.container.submodule(*([-y for y in x[:r]] for x in nonzero)) - - -def syzygy_module_groebner_basis_mat(m, generators, ring): + first_r = [[-y for y in x[:r]] for x in syz] + # Quickly remove zeros here. Call minimal_generating_set to reduce further. + nonzero = [x for x in first_r if any(y != m1.ring.zero for y in x)] + return m1.container.submodule(*nonzero) + + +def _convert_to_matrix(module, *generators): + result = [] + for syzygy in module: + row = [] + for dmp in syzygy.data: + try: + d = dmp.data.to_dict() + except AttributeError: + d = dmp.to_dict() + row.append(sympy.Poly(d, *generators, + domain=sympy.EX).as_expr()) + result.append(row) + return sympy.Matrix(result) + + +def syzygy_module_mat(m, generators, ring): """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 @@ -435,35 +464,24 @@ def syzygy_module_groebner_basis_mat(m, generators, ring): 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 - - def _convert_to_matrix(module, *generators): - result = [] - for syzygy in module: - row = [] - for dmp in syzygy.data: - row.append(sympy.Poly(dmp.data.to_dict(), *generators, - domain=sympy.EX).as_expr()) - result.append(row) - return sympy.Matrix(result) column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist()) for i in range(m.shape[1])] column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] - intersection = functools.reduce(_module_intersection, column_syzygy_modules) - return _convert_to_matrix(intersection.gens, *generators) + return _convert_to_matrix(minimal_generating_set(intersection).gens, *generators) -def factor_left(mat, axis_vars, ring): - """Return the left hand side of the factorisation of the matrix +def factor(mat, axis_vars, ring): + """Return a "rank-revealing" 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. 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. + have division either. The revealed rank might not be the actual + rank. 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. @@ -471,23 +489,14 @@ def factor_left(mat, axis_vars, ring): 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. """ - syzygy_of_mat = syzygy_module_groebner_basis_mat(mat, axis_vars, ring) + syzygy_of_mat = syzygy_module_mat(mat, axis_vars, ring) if len(syzygy_of_mat) == 0: raise ValueError("could not find a factorization") - return syzygy_module_groebner_basis_mat(syzygy_of_mat.T, axis_vars, ring).T - - -def factor_right(mat, factor_left): - """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. - """ + factor_left = syzygy_module_mat(syzygy_of_mat.T, axis_vars, ring).T mat = factor_left.LUsolve(sympy.Matrix(mat), iszerofunc=lambda x: x.simplify() == 0) - return mat.applyfunc(lambda x: x.simplify()) + factor_right = mat.applyfunc(lambda x: x.simplify()) + return factor_left, factor_right # }}} diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 49bd0e99e..4c2c21f1e 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -483,11 +483,11 @@ def test_mindlin(): def test_paper_reduce_example(): from sympy import symbols, Matrix, EX from pytential.symbolic.pde.systems.reduce import \ - syzygy_module_groebner_basis_mat + syzygy_module_mat y = y1, y2, y3 = symbols("y1, y2, y3") m = Matrix([[y1 * y2, -2*y1**2-2*y3**2], [y1*y3, 2*y2*y3]]) poly_ring = EX.old_poly_ring(*y) ring = poly_ring / [y1**2 + y2**2 + y3**2] - fm = syzygy_module_groebner_basis_mat(m, y, ring) - lhs = syzygy_module_groebner_basis_mat(fm.T, y, ring).T + fm = syzygy_module_mat(m, y, ring) + lhs = syzygy_module_mat(fm.T, y, ring).T assert lhs == Matrix([[y2], [y3]]) From e79243ea7bdc77a1c8bc6628546214e5784c2ef9 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 4 Apr 2023 18:19:06 -0500 Subject: [PATCH 146/156] Use in_terms_of_generator instead of LUsolve --- pytential/symbolic/pde/systems/reduce.py | 36 +++++++++++++++--------- test/test_pde_system_utils.py | 6 ++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index cf7f46255..ad4d7c164 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -444,7 +444,7 @@ def _convert_to_matrix(module, *generators): result = [] for syzygy in module: row = [] - for dmp in syzygy.data: + for dmp in syzygy: try: d = dmp.data.to_dict() except AttributeError: @@ -455,11 +455,10 @@ def _convert_to_matrix(module, *generators): return sympy.Matrix(result) -def syzygy_module_mat(m, generators, ring): +def syzygy_module(m, generators, ring): """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 and is a left nullspace of the matrix. + represented as a matrix and returns the syzygy module. + The syzygy module *S* 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. @@ -471,7 +470,7 @@ def syzygy_module_mat(m, generators, ring): intersection = functools.reduce(_module_intersection, column_syzygy_modules) - return _convert_to_matrix(minimal_generating_set(intersection).gens, *generators) + return minimal_generating_set(intersection) def factor(mat, axis_vars, ring): @@ -487,16 +486,25 @@ def factor(mat, axis_vars, ring): 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. + the syzygy module of S.T and to get L we get the transpose of that. """ - syzygy_of_mat = syzygy_module_mat(mat, axis_vars, ring) - if len(syzygy_of_mat) == 0: + S_module = syzygy_module(mat, axis_vars, ring) + S = _convert_to_matrix(S_module.gens, *axis_vars) + if len(S) == 0: raise ValueError("could not find a factorization") - factor_left = syzygy_module_mat(syzygy_of_mat.T, axis_vars, ring).T - mat = factor_left.LUsolve(sympy.Matrix(mat), - iszerofunc=lambda x: x.simplify() == 0) - factor_right = mat.applyfunc(lambda x: x.simplify()) - return factor_left, factor_right + L_t_module = syzygy_module(S.T, axis_vars, ring) + L_t = _convert_to_matrix(L_t_module.gens, *axis_vars) + R_t_module = [L_t_module.in_terms_of_generators(mat[:, i]) + for i in range(mat.shape[1])] + R_t = _convert_to_matrix(R_t_module, *axis_vars) + + if 0: + R2 = L_t.T.LUsolve(sympy.Matrix(mat), + iszerofunc=lambda x: x.simplify() == 0) + R2 = R2.applyfunc(lambda x: x.simplify()) + return L_t.T, R2 + + return L_t.T, R_t.T # }}} diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 4c2c21f1e..26591befa 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -482,12 +482,10 @@ def test_mindlin(): def test_paper_reduce_example(): from sympy import symbols, Matrix, EX - from pytential.symbolic.pde.systems.reduce import \ - syzygy_module_mat + from pytential.symbolic.pde.systems.reduce import factor y = y1, y2, y3 = symbols("y1, y2, y3") m = Matrix([[y1 * y2, -2*y1**2-2*y3**2], [y1*y3, 2*y2*y3]]) poly_ring = EX.old_poly_ring(*y) ring = poly_ring / [y1**2 + y2**2 + y3**2] - fm = syzygy_module_mat(m, y, ring) - lhs = syzygy_module_mat(fm.T, y, ring).T + lhs, rhs = factor(m, y, ring) assert lhs == Matrix([[y2], [y3]]) From 6480c5f205dbe5aec742158bd3e06a9b9373a328 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Tue, 4 Apr 2023 18:40:22 -0500 Subject: [PATCH 147/156] test factoring without PDE --- pytential/symbolic/pde/systems/reduce.py | 2 +- test/test_pde_system_utils.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index ad4d7c164..0d37238bb 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -491,7 +491,7 @@ def factor(mat, axis_vars, ring): S_module = syzygy_module(mat, axis_vars, ring) S = _convert_to_matrix(S_module.gens, *axis_vars) if len(S) == 0: - raise ValueError("could not find a factorization") + return mat, sympy.eye(mat.shape[1]) L_t_module = syzygy_module(S.T, axis_vars, ring) L_t = _convert_to_matrix(L_t_module.gens, *axis_vars) R_t_module = [L_t_module.in_terms_of_generators(mat[:, i]) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 26591befa..04dff96cf 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -487,5 +487,10 @@ def test_paper_reduce_example(): m = Matrix([[y1 * y2, -2*y1**2-2*y3**2], [y1*y3, 2*y2*y3]]) poly_ring = EX.old_poly_ring(*y) ring = poly_ring / [y1**2 + y2**2 + y3**2] + lhs, rhs = factor(m, y, ring) assert lhs == Matrix([[y2], [y3]]) + + # When the PDE is not taken into account, rank is 2 + lhs, rhs = factor(m, y, poly_ring) + assert lhs == m From edbbd20161d632def3ee6124bbd48c3ed2efbff6 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 11:36:28 -0500 Subject: [PATCH 148/156] Use more of sympy's syzygy functionality --- pytential/symbolic/pde/systems/reduce.py | 31 +++--------------------- test/test_pde_system_utils.py | 12 ++++----- 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 0d37238bb..fc316db15 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -28,7 +28,6 @@ from pymbolic.geometric_algebra.mapper import WalkMapper from pymbolic.primitives import Product import sympy -import functools from collections import defaultdict from math import prod @@ -420,26 +419,6 @@ def minimal_generating_set(m): return res -def _module_intersection(m1, m2): - # Copyright: SymPy developers - # License: BSD-3-Clause - # See: [A Singular Introduction to Commutative Algebra, section 2.8.2] - fi = m1.gens - hi = m2.gens - r = m1.rank - ci = [[0]*(2*r) for _ in range(r)] - for k in range(r): - ci[k][k] = 1 - ci[k][r + k] = 1 - di = [list(f) + [0]*r for f in fi] - ei = [[0]*r + list(h) for h in hi] - syz = m1.ring.free_module(2*r).submodule(*(ci + di + ei))._syzygies() - first_r = [[-y for y in x[:r]] for x in syz] - # Quickly remove zeros here. Call minimal_generating_set to reduce further. - nonzero = [x for x in first_r if any(y != m1.ring.zero for y in x)] - return m1.container.submodule(*nonzero) - - def _convert_to_matrix(module, *generators): result = [] for syzygy in module: @@ -464,13 +443,9 @@ def syzygy_module(m, generators, ring): unrelated symbols like *mu* in the expression, we need to use a symbolic domain. """ - column_ideals = [ring.free_module(1).submodule(*m[:, i].tolist()) - for i in range(m.shape[1])] - column_syzygy_modules = [ideal.syzygy_module() for ideal in column_ideals] - intersection = functools.reduce(_module_intersection, - column_syzygy_modules) - - return minimal_generating_set(intersection) + module = ring.free_module(m.shape[1]).submodule( + *[m[i, :] for i in range(m.shape[0])]) + return minimal_generating_set(module.syzygy_module()) def factor(mat, axis_vars, ring): diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 04dff96cf..59ac00e0d 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -349,13 +349,13 @@ def test_merge_different_qbx_forced_limit(): 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_g6 = int_g_vec(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) - int_g10 = int_g6.copy(qbx_forced_limit=-2) * (-1) \ - + int_g7.copy(qbx_forced_limit=2) * (-1) + int_g8 = int_g7 + int_g6 + int_g9 = int_g6.copy(qbx_forced_limit=2) \ + + int_g7.copy(qbx_forced_limit=-2) + int_g10 = int_g6.copy(qbx_forced_limit=-2) \ + + int_g7.copy(qbx_forced_limit=2) assert result[0] == int_g8 assert result[1] == int_g9 From 04f6836dd4dda2e739a61035d7f450b4196c743e Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 11:54:09 -0500 Subject: [PATCH 149/156] comment on minimal_generating_set --- pytential/symbolic/pde/systems/reduce.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index fc316db15..3da2ed647 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -407,6 +407,10 @@ def _kernel_source_derivs_as_poly(kernel, axis_vars): # {{{ factor the matrix def minimal_generating_set(m): + """Computes a module with a minimal generating set as its generators + from an input module with possibly redundant generators. The output + does not necessarily have the smallest minimal generating set. + """ gens = list(m.gens) nonzero = [x for x in gens if any(y != m.ring.zero for y in x)] basis = nonzero[:] From b6b3e083ef2cf87a345219f568c8053ab52d1435 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 14:55:24 -0500 Subject: [PATCH 150/156] fix for poly being zero --- pytential/symbolic/pde/systems/reduce.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 3da2ed647..d375fa759 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -146,11 +146,14 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): source_kernels = [] densities = [] for j in range(right_factor.shape[1]): + if source_int_gs[i][j] == 0: + continue new_densities = [density * source_exprs[j] for density in source_int_gs[i][j].densities] source_kernels.extend(source_int_gs[i][j].source_kernels) densities.extend(new_densities) - source_int_gs_merged.append(source_int_gs[i][0].copy( + nonzero_intg = source_int_gs[i][j] + source_int_gs_merged.append(nonzero_intg.copy( source_kernels=tuple(source_kernels), densities=tuple(densities))) # Now that we have the IntG expressions depending on the source @@ -497,6 +500,10 @@ def _convert_source_poly_to_int_g_derivs(poly, orig_int_g, axis_vars): and then to a :class:`~pytential.symbolic.primitives.IntG`. """ from pytential.symbolic.pde.systems.merge import simplify_densities + + if poly == 0: + return 0 + to_pymbolic = SympyToPymbolicMapper() orig_kernel = orig_int_g.source_kernels[0] From 7c50cdc1529bbd48ad54f98f2893333fb90fa82d Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 14:56:03 -0500 Subject: [PATCH 151/156] fix getting minimal generating set --- pytential/symbolic/pde/systems/reduce.py | 35 ++++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index d375fa759..7c139fcf2 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -422,8 +422,7 @@ def minimal_generating_set(m): others.remove(x) if x in m.container.submodule(*others): basis = others - res = m.container.submodule(*basis) - return res + return m.container.submodule(*basis) def _convert_to_matrix(module, *generators): @@ -465,28 +464,34 @@ def factor(mat, axis_vars, ring): rank. 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. + such that S M.T = 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 L we get the transpose of that. + Then, M S.T = 0 which implies that M is in the space spanned by + the syzygy module of S.T and to get R we get the transpose of that. """ - S_module = syzygy_module(mat, axis_vars, ring) + if mat.shape[0] < mat.shape[1]: + # For sympy performance, we use a tall and skinny matrix + L, R = factor(mat.T, axis_vars, ring) + return R.T, L.T + + S_module = syzygy_module(mat.T, axis_vars, ring) S = _convert_to_matrix(S_module.gens, *axis_vars) + if len(S) == 0: return mat, sympy.eye(mat.shape[1]) - L_t_module = syzygy_module(S.T, axis_vars, ring) - L_t = _convert_to_matrix(L_t_module.gens, *axis_vars) - R_t_module = [L_t_module.in_terms_of_generators(mat[:, i]) - for i in range(mat.shape[1])] - R_t = _convert_to_matrix(R_t_module, *axis_vars) + R_module = syzygy_module(S.T, axis_vars, ring) + R = _convert_to_matrix(R_module.gens, *axis_vars) + L_module = [R_module.in_terms_of_generators(mat[i, :]) + for i in range(mat.shape[0])] + L = _convert_to_matrix(L_module, *axis_vars) if 0: - R2 = L_t.T.LUsolve(sympy.Matrix(mat), + L2 = R.LUsolve(sympy.Matrix(mat), iszerofunc=lambda x: x.simplify() == 0) - R2 = R2.applyfunc(lambda x: x.simplify()) - return L_t.T, R2 + L2 = L2.applyfunc(lambda x: x.simplify()) + return L2, R - return L_t.T, R_t.T + return L, R # }}} From 5245ce2c3e249018f7d0c85a1a73f1fc483213c4 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 15:33:45 -0500 Subject: [PATCH 152/156] fix tests --- test/test_pde_system_utils.py | 42 ++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 59ac00e0d..ece24905d 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -202,24 +202,21 @@ def test_reduce_number_of_fmms(): 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=[]) + result = merge_int_g_exprs([int_g1, int_g2], + source_dependent_variables=densities) + assert get_number_of_fmms(result) == 1 int_g3 = \ IntG(target_kernel=AxisTargetDerivative(1, knl), - source_kernels=[AxisSourceDerivative(0, knl), - AxisSourceDerivative(1, knl)], - densities=[-mu * densities[0], -mu * densities[1]], + source_kernels=[AxisSourceDerivative(1, knl), + AxisSourceDerivative(0, knl)], + densities=[-densities[1], -densities[0]], 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) + int_g4 = int_g3.copy(target_kernel=AxisTargetDerivative(2, knl)) - assert result[0] == int_g3 - assert result[1] == int_g4 * mu**(-1) + assert result[0] == int_g3 * mu + assert result[1] == int_g4 def test_source_dependent_variable(): @@ -321,7 +318,7 @@ def test_merge_different_kernels(): density, qbx_forced_limit=1) result = merge_int_g_exprs([int_g1, int_g2], - source_dependent_variables=[]) + source_dependent_variables=[density]) int_g3 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) \ + IntG(target_kernel=helmholtz_knl, @@ -333,6 +330,9 @@ def test_merge_different_kernels(): assert result[0] == int_g3 assert result[1] == int_g2 + result_no_sdv = merge_int_g_exprs([int_g1, int_g2]) + assert result == result_no_sdv + def test_merge_different_qbx_forced_limit(): dim = 3 @@ -347,7 +347,7 @@ def test_merge_different_qbx_forced_limit(): 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=[]) + source_dependent_variables=[density]) int_g6 = int_g_vec(laplace_knl, density, qbx_forced_limit=1) int_g7 = int_g6.copy(target_kernel=AxisTargetDerivative(0, laplace_knl)) @@ -361,6 +361,9 @@ def test_merge_different_qbx_forced_limit(): assert result[1] == int_g9 assert result[2] == int_g10 + result_no_sdv = merge_int_g_exprs([int_g3, int_g4, int_g5]) + assert result == result_no_sdv + def test_merge_directional_source(): from pymbolic.primitives import Variable @@ -416,14 +419,17 @@ def test_restoring_target_attributes(): density, qbx_forced_limit=1) result = merge_int_g_exprs([int_g1, int_g2], - source_dependent_variables=[]) + source_dependent_variables=[density]) assert result[0] == int_g1 assert result[1] == int_g2 + result_no_sdv = merge_int_g_exprs([int_g1, int_g2]) + assert result == result_no_sdv + def test_int_gs_in_densities(): - from pymbolic.primitives import Variable, Quotient + from pymbolic.primitives import Variable dim = 3 laplace_knl = LaplaceKernel(dim) density = Variable("density") @@ -445,9 +451,9 @@ def test_int_gs_in_densities(): source_kernels = [AxisSourceDerivative(0, laplace_knl), laplace_knl] densities = [ (-1)*int_g_vec(AxisTargetDerivative(1, laplace_knl), - (-2)*density, qbx_forced_limit=1), + density, qbx_forced_limit=1) * (-2), int_g_vec(AxisTargetDerivative(2, laplace_knl), - (-2)*density, qbx_forced_limit=1) * Quotient(1, 2) + density, qbx_forced_limit=1) * (-1) ] int_g3 = IntG(target_kernel=laplace_knl, source_kernels=tuple(source_kernels), From 7c0e8502ef9b6ccf8d562d3c58e57d7652ef9dad Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 18:51:46 -0500 Subject: [PATCH 153/156] debug --- test/test_pde_system_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index ece24905d..0538cd41d 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -460,6 +460,7 @@ def test_int_gs_in_densities(): densities=tuple(densities), qbx_forced_limit=1) + print(result[0]) assert result[0] == int_g3 From 714da543d90835f48902dcf1efb0ddb6917b58b5 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 19:54:57 -0500 Subject: [PATCH 154/156] add Fu et al method --- pytential/symbolic/elasticity.py | 169 +++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/pytential/symbolic/elasticity.py b/pytential/symbolic/elasticity.py index c5615dc44..fa6405b60 100644 --- a/pytential/symbolic/elasticity.py +++ b/pytential/symbolic/elasticity.py @@ -416,6 +416,7 @@ class Method(Enum): naive = 1 laplace = 2 biharmonic = 3 + laplace_slow = 4 def make_elasticity_wrapper( @@ -449,6 +450,12 @@ def make_elasticity_wrapper( else: return ElasticityWrapperYoshida(dim=dim, mu=mu, nu=nu) + elif method == Method.laplace_slow: + if nu == 0.5: + raise ValueError("invalid value of nu=0.5 for method laplace_slow") + else: + return ElasticityWrapperFu(dim=dim, + mu=mu, nu=nu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -486,6 +493,12 @@ def make_elasticity_double_layer_wrapper( else: return ElasticityDoubleLayerWrapperYoshida(dim=dim, mu=mu, nu=nu) + elif method == Method.laplace_slow: + if nu == 0.5: + raise ValueError("invalid value of nu=0.5 for method laplace_slow") + else: + return ElasticityDoubleLayerWrapperFu(dim=dim, + mu=mu, nu=nu) else: raise ValueError(f"invalid method: {method}." "Needs to be one of naive, laplace, biharmonic") @@ -641,6 +654,162 @@ def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, extra_deriv_dirs) + +# }}} + +# {{{ Fu + +@dataclass +class ElasticityDoubleLayerWrapperFu(ElasticityDoubleLayerWrapperBase): + r"""ElasticityDoubleLayer Wrapper using Fu et al's method [1] which uses + Laplace derivatives. + + [1] Fu, Y., Klimkowski, K. J., Rodin, G. J., Berger, E., Browne, J. C., + Singer, J. K., ... & Vemaganti, K. S. (1998). A fast solution method for + three‐dimensional many‐particle problems of linear elasticity. + International Journal for Numerical Methods in Engineering, 42(7), 1215-1229. + """ + dim: int + mu: ExpressionT + nu: ExpressionT + + def __post_init__(self): + if not self.dim == 3: + raise ValueError("unsupported dimension given to " + "ElasticityDoubleLayerWrapperFu: {self.dim}") + + @cached_property + def laplace_kernel(self): + return LaplaceKernel(dim=3) + + def apply(self, density_vec_sym, dir_vec_sym, qbx_forced_limit, + extra_deriv_dirs=()): + return self.apply_single_and_double_layer([0]*self.dim, + density_vec_sym, dir_vec_sym, qbx_forced_limit, 0, 1, + extra_deriv_dirs) + + def apply_single_and_double_layer(self, stokeslet_density_vec_sym, + stresslet_density_vec_sym, dir_vec_sym, + qbx_forced_limit, stokeslet_weight, stresslet_weight, + extra_deriv_dirs=()): + + mu = self.mu + nu = self.nu + stokeslet_weight *= -1 + + 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): + res = -int_g.copy(target_kernel=add_extra_deriv_dirs( + TargetPointMultiplier(j, + AxisTargetDerivative(i, int_g.target_kernel)))) + if i == j: + res += (3 - 4*nu)*int_g.copy( + target_kernel=add_extra_deriv_dirs(int_g.target_kernel)) + return res / (4*mu*(1 - nu)) + + def Q(i, int_g): + res = int_g.copy(target_kernel=add_extra_deriv_dirs( + AxisTargetDerivative(i, int_g.target_kernel))) + return res / (4*mu*(1 - nu)) + + def R(i, j, p, int_g): + res = int_g.copy(target_kernel=add_extra_deriv_dirs( + TargetPointMultiplier(j, AxisTargetDerivative(i, + AxisTargetDerivative(p, int_g.target_kernel))))) + if j == p: + res += (1 - 2*nu)*int_g.copy(target_kernel=add_extra_deriv_dirs( + AxisTargetDerivative(i, int_g.target_kernel))) + if i == j: + res -= (1 - 2*nu)*int_g.copy(target_kernel=add_extra_deriv_dirs( + AxisTargetDerivative(p, int_g.target_kernel))) + if i == p: + res -= 2*(1 - nu)*int_g.copy(target_kernel=add_extra_deriv_dirs( + AxisTargetDerivative(j, int_g.target_kernel))) + return res / (2*mu*(1 - nu)) + + def S(i, p, int_g): + res = int_g.copy(target_kernel=add_extra_deriv_dirs( + AxisTargetDerivative(i, + AxisTargetDerivative(p, int_g.target_kernel)))) + return res / (-2*mu*(1 - nu)) + + sym_expr = np.zeros((3,), dtype=object) + + kernel = self.laplace_kernel + source = sym.nodes(3).as_vector() + normal = dir_vec_sym + sigma = stresslet_density_vec_sym + + for i in range(3): + for j in range(3): + density = stokeslet_weight * stokeslet_density_vec_sym[j] + int_g = sym.IntG(target_kernel=kernel, + source_kernels=(kernel,), + densities=(density,), + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += P(i, j, int_g) + + density = sum(stokeslet_weight + * stokeslet_density_vec_sym[j] * source[j] for j in range(3)) + int_g = sym.IntG(target_kernel=kernel, + source_kernels=(kernel,), + densities=(density,), + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += Q(i, int_g) + + for j in range(3): + for p in range(3): + density = stresslet_weight * normal[p] * sigma[j] + int_g = sym.IntG(target_kernel=kernel, + source_kernels=(kernel,), + densities=(density,), + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += R(i, j, p, int_g) + + for p in range(3): + density = sum(stresslet_weight * normal[p] * sigma[j] * source[j] + for j in range(3)) + int_g = sym.IntG(target_kernel=kernel, + source_kernels=(kernel,), + densities=(density,), + qbx_forced_limit=qbx_forced_limit) + sym_expr[i] += S(i, p, int_g) + + return sym_expr + + +@dataclass +class ElasticityWrapperFu(ElasticityWrapperBase): + r"""Elasticity single layer using Fu et al's method [1] which uses + Laplace derivatives. + + [1] Fu, Y., Klimkowski, K. J., Rodin, G. J., Berger, E., Browne, J. C., + Singer, J. K., ... & Vemaganti, K. S. (1998). A fast solution method for + three‐dimensional many‐particle problems of linear elasticity. + International Journal for Numerical Methods in Engineering, 42(7), 1215-1229. + """ + dim: int + mu: ExpressionT + nu: ExpressionT + + def __post_init__(self): + if not self.dim == 3: + raise ValueError("unsupported dimension given to " + f"ElasticityDoubleLayerWrapperFu: {self.dim}") + + @cached_property + def stresslet(self): + return ElasticityDoubleLayerWrapperFu(3, self.mu, self.nu) + + def apply(self, density_vec_sym, qbx_forced_limit, extra_deriv_dirs=()): + return self.stresslet.apply_single_and_double_layer(density_vec_sym, + [0]*self.dim, [0]*self.dim, qbx_forced_limit, 1, 0, + extra_deriv_dirs) + # }}} From ce380e95f042511a7ea2c17fbbc8b7507f731664 Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Wed, 5 Apr 2023 20:08:30 -0500 Subject: [PATCH 155/156] fix test for new pymbolic --- test/test_pde_system_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_pde_system_utils.py b/test/test_pde_system_utils.py index 0538cd41d..3b62518ef 100644 --- a/test/test_pde_system_utils.py +++ b/test/test_pde_system_utils.py @@ -429,7 +429,7 @@ def test_restoring_target_attributes(): def test_int_gs_in_densities(): - from pymbolic.primitives import Variable + from pymbolic.primitives import Variable, Product dim = 3 laplace_knl = LaplaceKernel(dim) density = Variable("density") @@ -449,9 +449,10 @@ def test_int_gs_in_densities(): result = merge_int_g_exprs([int_g1]) source_kernels = [AxisSourceDerivative(0, laplace_knl), laplace_knl] + densities = [ - (-1)*int_g_vec(AxisTargetDerivative(1, laplace_knl), - density, qbx_forced_limit=1) * (-2), + Product((-1, Product((int_g_vec(AxisTargetDerivative(1, laplace_knl), + density, qbx_forced_limit=1), -2)))), int_g_vec(AxisTargetDerivative(2, laplace_knl), density, qbx_forced_limit=1) * (-1) ] @@ -460,7 +461,6 @@ def test_int_gs_in_densities(): densities=tuple(densities), qbx_forced_limit=1) - print(result[0]) assert result[0] == int_g3 From 9c1fcb442d3383883a9f2aea9f9335e3fdbb8f1f Mon Sep 17 00:00:00 2001 From: Isuru Fernando Date: Thu, 6 Apr 2023 18:19:42 -0500 Subject: [PATCH 156/156] Fix big typo --- pytential/symbolic/pde/systems/reduce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytential/symbolic/pde/systems/reduce.py b/pytential/symbolic/pde/systems/reduce.py index 7c139fcf2..75c352569 100644 --- a/pytential/symbolic/pde/systems/reduce.py +++ b/pytential/symbolic/pde/systems/reduce.py @@ -127,7 +127,7 @@ def reduce_number_of_fmms(int_gs, source_dependent_variables): # # If k is greater than or equal to n we are gaining nothing. # Return as is. - if right_factor.shape[0] >= mat.shape[0]: + if right_factor.shape[0] >= mat.shape[1]: return int_gs base_kernel = int_gs[0].source_kernels[0].get_base_kernel()