Skip to content

Commit 4207c9b

Browse files
ShawnL00inducer
authored andcommitted
add a test case for the qbmax line expansion
1 parent 2077b11 commit 4207c9b

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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

Comments
 (0)