Skip to content

Commit 9406a60

Browse files
committed
line expansion update
1 parent c3e1a73 commit 9406a60

2 files changed

Lines changed: 322 additions & 1 deletion

File tree

sumpy/expansion/local.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import math
2828
from abc import ABC, abstractmethod
2929
from dataclasses import dataclass, field
30-
from typing import TYPE_CHECKING, cast
30+
from typing import TYPE_CHECKING, Any, cast
3131

3232
from typing_extensions import override
3333

@@ -67,6 +67,7 @@
6767
.. autoclass:: H2DLocalExpansion
6868
.. autoclass:: Y2DLocalExpansion
6969
.. autoclass:: LineTaylorLocalExpansion
70+
.. autoclass:: AsymptoticDividingLineTaylorExpansion
7071
"""
7172

7273

@@ -113,7 +114,10 @@ def translate_from(self,
113114

114115
# {{{ line taylor
115116

117+
@dataclass(frozen=True)
116118
class LineTaylorLocalExpansion(LocalExpansionBase, ABC):
119+
tau: float = field(kw_only=True, default=1)
120+
117121
@property
118122
@override
119123
def m2l_translation(self) -> M2LTranslationBase:
@@ -180,8 +184,10 @@ def evaluate(self,
180184

181185
# NOTE: We can't meaningfully apply target derivatives here.
182186
# Instead, this is handled in LayerPotentialBase._evaluate.
187+
183188
return sym.Add(*(
184189
coeffs[self.get_storage_index(i)] / math.factorial(i[0])
190+
* self.tau**i[0]
185191
for i in self.get_coefficient_identifiers()))
186192

187193
@override
@@ -200,6 +206,95 @@ def translate_from(self,
200206
# }}}
201207

202208

209+
# {{{ Asymptotic dividing line Taylor expansion
210+
211+
@dataclass(frozen=True)
212+
class AsymptoticDividingLineTaylorExpansion(LineTaylorLocalExpansion):
213+
r"""
214+
A target-specific modified line Taylor expansion.
215+
216+
The expansion line is defined as :math:`l(\tau) = \text{avec} + \tau \cdot
217+
\text{bvec}` at a target point :math:`x`. The modified line Taylor expansion takes
218+
the form:
219+
220+
.. math::
221+
222+
\sum_{k=0}^{\text{order}} \frac{g_k}{k!} \tau^k,
223+
224+
where:
225+
226+
.. math::
227+
228+
g_k := \frac{d^k}{d\tau^k} \left(
229+
\frac{\text{kernel}(l(\tau))}{\text{asymptotic}(l(\tau))}
230+
\right) \bigg|_{\tau=0}
231+
232+
.. automethod:: get_asymptotic_expression
233+
"""
234+
235+
asymptotic: Any = field(kw_only=True)
236+
237+
def get_asymptotic_expression(self, scaled_dist_vec: sym.Matrix) -> sym.Expr:
238+
from sumpy.symbolic import PymbolicToSympyMapperWithSymbols, Symbol
239+
240+
expr = PymbolicToSympyMapperWithSymbols()(self.asymptotic)
241+
expr = expr.xreplace({Symbol(f"d{i}"): dist_vec_i
242+
for i, dist_vec_i in enumerate(scaled_dist_vec)})
243+
244+
tau = sym.Symbol("tau")
245+
246+
b = scaled_dist_vec.applyfunc(lambda expr: expr.coeff(tau))
247+
a = scaled_dist_vec - tau*b
248+
expr = expr.subs({Symbol(f"a{i}"): a_i for i, a_i in enumerate(a)})
249+
expr = expr.subs({Symbol(f"b{i}"): b_i for i, b_i in enumerate(b)})
250+
251+
return expr
252+
253+
@override
254+
def coefficients_from_source(self,
255+
kernel: Kernel,
256+
avec: sym.Matrix,
257+
bvec: sym.Matrix | None,
258+
rscale: sym.Expr,
259+
sac: SymbolicAssignmentCollection | None = None
260+
) -> Sequence[sym.Expr]:
261+
# no point in heeding rscale here--just ignore it
262+
if bvec is None:
263+
raise RuntimeError("cannot use line-Taylor expansions in a setting "
264+
"where the center-target vector is not known at coefficient "
265+
"formation")
266+
267+
tau = sym.Symbol("tau")
268+
269+
avec_line = cast("sym.Matrix", avec + tau*bvec)
270+
line_kernel = (
271+
kernel.get_expression(avec_line)
272+
/ self.get_asymptotic_expression(avec_line))
273+
274+
from sumpy.symbolic import USE_SYMENGINE
275+
if USE_SYMENGINE:
276+
from sumpy.derivative_taker import ExprDerivativeTaker
277+
deriv_taker = ExprDerivativeTaker(line_kernel, (tau,), sac=sac,
278+
rscale=sym.sympify(1))
279+
280+
return [kernel.postprocess_at_source(deriv_taker.diff(i), avec)
281+
.subs(tau, 0)
282+
for i in self.get_coefficient_identifiers()]
283+
else:
284+
# Workaround for sympy. The automatic distribution after
285+
# single-variable diff makes the expressions very large
286+
# (https://github.com/sympy/sympy/issues/4596), so avoid doing
287+
# single variable diff.
288+
#
289+
# See also https://gitlab.tiker.net/inducer/pytential/merge_requests/12
290+
291+
return [kernel.postprocess_at_source(line_kernel.diff(tau, i), avec)
292+
.subs(tau, 0)
293+
for i, in self.get_coefficient_identifiers()]
294+
295+
# }}}
296+
297+
203298
# {{{ volume taylor
204299

205300
@dataclass(frozen=True)
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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 = 4
118+
upsampling_factor = 4
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, qbx_order, asymptotic=asym_knl, 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+
expansion=asym_expn,
172+
source_kernels=(knl,),
173+
target_kernels=(knl,))
174+
175+
mat_asym_in, = mat_asym_gen(
176+
actx,
177+
targets=targets,
178+
sources=actx.from_numpy(sources_h),
179+
expansion_radii=actx.from_numpy(expansion_radii_h),
180+
centers=centers_in,
181+
**extra_kwargs)
182+
183+
mat_asym_in = actx.to_numpy(mat_asym_in)
184+
weighted_mat_asym_in = mat_asym_in * sigma[None, :]
185+
asym_eval_in = (np.sum(weighted_mat_asym_in, axis=1) *
186+
np.exp(-lam * expansion_radii_h * (1 - tau)))
187+
188+
mat_asym_out, = mat_asym_gen(
189+
actx,
190+
targets=targets,
191+
sources=actx.from_numpy(sources_h),
192+
expansion_radii=actx.from_numpy(expansion_radii_h),
193+
centers=centers_out,
194+
**extra_kwargs)
195+
196+
mat_asym_out = actx.to_numpy(mat_asym_out)
197+
weighted_mat_asym_out = mat_asym_out * sigma[None, :]
198+
asym_eval_out = (np.sum(weighted_mat_asym_out, axis=1) *
199+
np.exp(-lam * expansion_radii_h * (1 - tau)))
200+
201+
utrue_in = utrue(lam, expansion_radii_h, tau, targets_h, f_mode, -1)
202+
utrue_out = utrue(lam, expansion_radii_h, tau, targets_h, f_mode, 1)
203+
204+
err_in = (np.max(np.abs(asym_eval_in - utrue_in)) /
205+
np.max(np.abs(utrue_in)))
206+
err_out = (np.max(np.abs(asym_eval_out - utrue_out)) /
207+
np.max(np.abs(utrue_out)))
208+
209+
h_max = actx.to_numpy(
210+
bind(places, sym.h_max(places.ambient_dim))(actx))
211+
212+
eoc_in.add_data_point(h_max, err_in)
213+
eoc_out.add_data_point(h_max, err_out)
214+
215+
assert eoc_in.order_estimate() > qbx_order, \
216+
f"Interior convergence too slow: {eoc_in.order_estimate()}"
217+
218+
assert eoc_out.order_estimate() > qbx_order, \
219+
f"Exterior convergence too slow: {eoc_out.order_estimate()}"
220+
221+
222+
if __name__ == "__main__":
223+
if len(sys.argv) > 1:
224+
exec(sys.argv[1])
225+
else:
226+
pytest.main([__file__])

0 commit comments

Comments
 (0)