| 
 | 1 | +from __future__ import annotations  | 
 | 2 | + | 
 | 3 | + | 
 | 4 | +__copyright__ = """  | 
 | 5 | +Copyright (C) 2025 Shawn Lin  | 
 | 6 | +Copyright (C) 2025 University of Illinois Board of Trustees  | 
 | 7 | +"""  | 
 | 8 | + | 
 | 9 | +__license__ = """  | 
 | 10 | +Permission is hereby granted, free of charge, to any person obtaining a copy  | 
 | 11 | +of this software and associated documentation files (the "Software"), to deal  | 
 | 12 | +in the Software without restriction, including without limitation the rights  | 
 | 13 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell  | 
 | 14 | +copies of the Software, and to permit persons to whom the Software is  | 
 | 15 | +furnished to do so, subject to the following conditions:  | 
 | 16 | +
  | 
 | 17 | +The above copyright notice and this permission notice shall be included in  | 
 | 18 | +all copies or substantial portions of the Software.  | 
 | 19 | +
  | 
 | 20 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  | 
 | 21 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  | 
 | 22 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE  | 
 | 23 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER  | 
 | 24 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,  | 
 | 25 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN  | 
 | 26 | +THE SOFTWARE.  | 
 | 27 | +"""  | 
 | 28 | + | 
 | 29 | + | 
 | 30 | +import sys  | 
 | 31 | + | 
 | 32 | +import meshmode.mesh.generation as mgen  | 
 | 33 | +import mpmath  | 
 | 34 | +import numpy as np  | 
 | 35 | +import pytest  | 
 | 36 | +from meshmode.discretization import Discretization  | 
 | 37 | +from meshmode.discretization.poly_element import (  | 
 | 38 | +    InterpolatoryQuadratureSimplexGroupFactory,  | 
 | 39 | +)  | 
 | 40 | +from pytential import GeometryCollection, bind, sym  | 
 | 41 | +from pytential.qbx import QBXLayerPotentialSource  | 
 | 42 | + | 
 | 43 | +from arraycontext import (  | 
 | 44 | +    ArrayContextFactory,  | 
 | 45 | +    PyOpenCLArrayContext,  | 
 | 46 | +    flatten,  | 
 | 47 | +    pytest_generate_tests_for_array_contexts,  | 
 | 48 | +)  | 
 | 49 | +from pytools.convergence import EOCRecorder  | 
 | 50 | + | 
 | 51 | +from sumpy.array_context import (  # noqa: F401  | 
 | 52 | +    PytestPyOpenCLArrayContextFactory,  | 
 | 53 | +    _acf,  # pyright: ignore[reportUnusedImport]  | 
 | 54 | +)  | 
 | 55 | +from sumpy.expansion.local import AsymptoticDividingLineTaylorExpansion  | 
 | 56 | +from sumpy.kernel import YukawaKernel  | 
 | 57 | +from sumpy.qbx import LayerPotentialMatrixGenerator  | 
 | 58 | + | 
 | 59 | + | 
 | 60 | +pytest_generate_tests = pytest_generate_tests_for_array_contexts([  | 
 | 61 | +    PytestPyOpenCLArrayContextFactory,  | 
 | 62 | +    ])  | 
 | 63 | + | 
 | 64 | + | 
 | 65 | +def asym_yukawa(dim, lam=None):  | 
 | 66 | +    """Asymptotic function of the Yukawa kernel."""  | 
 | 67 | +    from pymbolic import primitives, var  | 
 | 68 | + | 
 | 69 | +    from sumpy.symbolic import SpatialConstant, pymbolic_real_norm_2  | 
 | 70 | + | 
 | 71 | +    b = pymbolic_real_norm_2(primitives.make_sym_vector("b", dim))  | 
 | 72 | + | 
 | 73 | +    if lam:  | 
 | 74 | +        expr = var("exp")(-lam * b * (1 - var("tau")))  | 
 | 75 | +    else:  | 
 | 76 | +        lam = SpatialConstant("lam")  | 
 | 77 | +        expr = var("exp")(-lam * b * (1 - var("tau")))  | 
 | 78 | +    return expr  | 
 | 79 | + | 
 | 80 | + | 
 | 81 | +def utrue(lam, r, tau, targets_h, f_mode, side):  | 
 | 82 | +    """Test convergence of QBMAX (asymptotic Yukawa expansion) on a unit circle  | 
 | 83 | +    with density φ(y) = exp(imθ_y)"""  | 
 | 84 | +    mpmath.mp.dps = 25  | 
 | 85 | + | 
 | 86 | +    angles = np.arctan2(targets_h[1, :], targets_h[0, :])  | 
 | 87 | +    n_points = len(angles)  | 
 | 88 | +    result = np.zeros(n_points, dtype=np.complex128)  | 
 | 89 | + | 
 | 90 | +    for i in range(n_points):  | 
 | 91 | +        r_i = float(r[i])  | 
 | 92 | + | 
 | 93 | +        if side == -1:  | 
 | 94 | +            coeff = float(mpmath.besselk(f_mode, lam) *  | 
 | 95 | +                         mpmath.besseli(f_mode, lam * (1 - (1 - tau) * r_i)))  | 
 | 96 | +        else:  | 
 | 97 | +            coeff = float(mpmath.besseli(f_mode, lam) *  | 
 | 98 | +                         mpmath.besselk(f_mode, lam * (1 + (1 - tau) * r_i)))  | 
 | 99 | + | 
 | 100 | +        result[i] = coeff * np.exp(1j * f_mode * angles[i])  | 
 | 101 | + | 
 | 102 | +    return result  | 
 | 103 | + | 
 | 104 | + | 
 | 105 | +def test_qbmax_yukawa_convergence(  | 
 | 106 | +            actx_factory: ArrayContextFactory,  | 
 | 107 | +        ):  | 
 | 108 | +    """Test convergence of QBMAX (asymptotic Yukawa expansion) for various τ values."""  | 
 | 109 | +    actx = actx_factory()  | 
 | 110 | +    if not isinstance(actx, PyOpenCLArrayContext):  | 
 | 111 | +        pytest.skip()  | 
 | 112 | + | 
 | 113 | +    lam = 15  | 
 | 114 | +    f_mode = 7  | 
 | 115 | +    nelements = [20, 40, 60]  | 
 | 116 | +    qbx_order = 4  | 
 | 117 | +    target_order = 5  | 
 | 118 | +    upsampling_factor = 5  | 
 | 119 | +    extra_kwargs = {"lam": lam}  | 
 | 120 | + | 
 | 121 | +    knl = YukawaKernel(2)  | 
 | 122 | +    asym_knl = asym_yukawa(2)  | 
 | 123 | + | 
 | 124 | +    rng = np.random.default_rng(seed=42)  | 
 | 125 | +    t = rng.uniform(0, 1, 10)  | 
 | 126 | +    targets_h = np.array([np.cos(2 * np.pi * t), np.sin(2 * np.pi * t)])  | 
 | 127 | +    targets = actx.from_numpy(targets_h)  | 
 | 128 | + | 
 | 129 | +    for tau in [0, 0.5, 1]:  | 
 | 130 | +        eoc_in = EOCRecorder()  | 
 | 131 | +        eoc_out = EOCRecorder()  | 
 | 132 | + | 
 | 133 | +        asym_expn = AsymptoticDividingLineTaylorExpansion(  | 
 | 134 | +            knl, asym_knl, qbx_order, tau=tau)  | 
 | 135 | + | 
 | 136 | +        for nelement in nelements:  | 
 | 137 | +            mesh = mgen.make_curve_mesh(  | 
 | 138 | +                mgen.circle, np.linspace(0, 1, nelement+1), target_order)  | 
 | 139 | +            pre_density_discr = Discretization(  | 
 | 140 | +                actx, mesh,  | 
 | 141 | +                InterpolatoryQuadratureSimplexGroupFactory(target_order))  | 
 | 142 | + | 
 | 143 | +            qbx = QBXLayerPotentialSource(  | 
 | 144 | +                pre_density_discr,  | 
 | 145 | +                upsampling_factor * target_order,  | 
 | 146 | +                qbx_order,  | 
 | 147 | +                fmm_order=False)  | 
 | 148 | + | 
 | 149 | +            places = GeometryCollection({"qbx": qbx}, auto_where=("qbx"))  | 
 | 150 | + | 
 | 151 | +            source_discr = places.get_discretization(  | 
 | 152 | +                "qbx", sym.QBX_SOURCE_QUAD_STAGE2)  | 
 | 153 | +            sources = source_discr.nodes()  | 
 | 154 | +            sources_h = actx.to_numpy(flatten(sources, actx)).reshape(2, -1)  | 
 | 155 | + | 
 | 156 | +            dofdesc = sym.DOFDescriptor("qbx", sym.QBX_SOURCE_QUAD_STAGE2)  | 
 | 157 | +            weights_nodes = bind(  | 
 | 158 | +                places,  | 
 | 159 | +                sym.weights_and_area_elements(  | 
 | 160 | +                    ambient_dim=2, dim=1, dofdesc=dofdesc))(actx)  | 
 | 161 | +            weights_nodes_h = actx.to_numpy(flatten(weights_nodes, actx))  | 
 | 162 | + | 
 | 163 | +            angle = np.arctan2(sources_h[1, :], sources_h[0, :])  | 
 | 164 | +            sigma = np.exp(1j * f_mode * angle) * weights_nodes_h  | 
 | 165 | + | 
 | 166 | +            expansion_radii_h = np.ones(targets_h.shape[1]) * np.pi / nelement  | 
 | 167 | +            centers_in = actx.from_numpy((1 - expansion_radii_h) * targets_h)  | 
 | 168 | +            centers_out = actx.from_numpy((1 + expansion_radii_h) * targets_h)  | 
 | 169 | + | 
 | 170 | +            mat_asym_gen = LayerPotentialMatrixGenerator(  | 
 | 171 | +                actx.context,  | 
 | 172 | +                expansion=asym_expn,  | 
 | 173 | +                source_kernels=(knl,),  | 
 | 174 | +                target_kernels=(knl,))  | 
 | 175 | + | 
 | 176 | +            _, (mat_asym_in,) = mat_asym_gen(  | 
 | 177 | +                actx.queue,  | 
 | 178 | +                targets=targets,  | 
 | 179 | +                sources=actx.from_numpy(sources_h),  | 
 | 180 | +                expansion_radii=expansion_radii_h,  | 
 | 181 | +                centers=centers_in,  | 
 | 182 | +                **extra_kwargs)  | 
 | 183 | + | 
 | 184 | +            mat_asym_in = actx.to_numpy(mat_asym_in)  | 
 | 185 | +            weighted_mat_asym_in = mat_asym_in * sigma[None, :]  | 
 | 186 | +            asym_eval_in = (np.sum(weighted_mat_asym_in, axis=1) *  | 
 | 187 | +                           np.exp(-lam * expansion_radii_h * (1 - tau)))  | 
 | 188 | + | 
 | 189 | +            _, (mat_asym_out,) = mat_asym_gen(  | 
 | 190 | +                actx.queue,  | 
 | 191 | +                targets=targets,  | 
 | 192 | +                sources=actx.from_numpy(sources_h),  | 
 | 193 | +                expansion_radii=expansion_radii_h,  | 
 | 194 | +                centers=centers_out,  | 
 | 195 | +                **extra_kwargs)  | 
 | 196 | + | 
 | 197 | +            mat_asym_out = actx.to_numpy(mat_asym_out)  | 
 | 198 | +            weighted_mat_asym_out = mat_asym_out * sigma[None, :]  | 
 | 199 | +            asym_eval_out = (np.sum(weighted_mat_asym_out, axis=1) *  | 
 | 200 | +                            np.exp(-lam * expansion_radii_h * (1 - tau)))  | 
 | 201 | + | 
 | 202 | +            utrue_in = utrue(lam, expansion_radii_h, tau, targets_h, f_mode, -1)  | 
 | 203 | +            utrue_out = utrue(lam, expansion_radii_h, tau, targets_h, f_mode, 1)  | 
 | 204 | + | 
 | 205 | +            err_in = (np.max(np.abs(asym_eval_in - utrue_in)) /  | 
 | 206 | +                     np.max(np.abs(utrue_in)))  | 
 | 207 | +            err_out = (np.max(np.abs(asym_eval_out - utrue_out)) /  | 
 | 208 | +                      np.max(np.abs(utrue_out)))  | 
 | 209 | + | 
 | 210 | +            h_max = actx.to_numpy(  | 
 | 211 | +                bind(places, sym.h_max(places.ambient_dim))(actx))  | 
 | 212 | + | 
 | 213 | +            eoc_in.add_data_point(h_max, err_in)  | 
 | 214 | +            eoc_out.add_data_point(h_max, err_out)  | 
 | 215 | + | 
 | 216 | +        assert eoc_in.order_estimate() > qbx_order, \  | 
 | 217 | +                f"Interior convergence too slow: {eoc_in.order_estimate()}"  | 
 | 218 | + | 
 | 219 | +        assert eoc_out.order_estimate() > qbx_order, \  | 
 | 220 | +                f"Exterior convergence too slow: {eoc_out.order_estimate()}"  | 
 | 221 | + | 
 | 222 | + | 
 | 223 | +if __name__ == "__main__":  | 
 | 224 | +    if len(sys.argv) > 1:  | 
 | 225 | +        exec(sys.argv[1])  | 
 | 226 | +    else:  | 
 | 227 | +        pytest.main([__file__])  | 
0 commit comments