Skip to content

Commit 04f3797

Browse files
ShawnL00inducer
authored andcommitted
l2l coeffs test
ruff check
1 parent 118ae83 commit 04f3797

1 file changed

Lines changed: 256 additions & 0 deletions

File tree

sumpy/test/test_l2l_coeffs.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"""
2+
Verification for the difference between compressed L2L coefficients and
3+
full L2L coefficients.
4+
"""
5+
from __future__ import annotations
6+
7+
8+
__copyright__ = """
9+
Copyright (C) 2026 Shawn/Chaoqi Lin
10+
"""
11+
12+
__license__ = """
13+
Permission is hereby granted, free of charge, to any person obtaining a copy
14+
of this software and associated documentation files (the "Software"), to deal
15+
in the Software without restriction, including without limitation the rights
16+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17+
copies of the Software, and to permit persons to whom the Software is
18+
furnished to do so, subject to the following conditions:
19+
20+
The above copyright notice and this permission notice shall be included in
21+
all copies or substantial portions of the Software.
22+
23+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
29+
THE SOFTWARE.
30+
"""
31+
32+
33+
import math
34+
import sys
35+
from typing import TYPE_CHECKING
36+
37+
import numpy as np
38+
import pytest
39+
import scipy.special as spsp
40+
import sympy as sp
41+
42+
from arraycontext import (
43+
pytest_generate_tests_for_array_contexts,
44+
)
45+
46+
import sumpy.toys as t
47+
from sumpy.array_context import PytestPyOpenCLArrayContextFactory, _acf # noqa: F401
48+
from sumpy.expansion.local import (
49+
LinearPDEConformingVolumeTaylorLocalExpansion,
50+
VolumeTaylorLocalExpansion,
51+
)
52+
from sumpy.kernel import (
53+
BiharmonicKernel,
54+
HelmholtzKernel,
55+
Kernel,
56+
LaplaceKernel,
57+
YukawaKernel,
58+
)
59+
from sumpy.tools import build_matrix
60+
61+
62+
if TYPE_CHECKING:
63+
from collections.abc import Mapping
64+
65+
from arraycontext import ArrayContextFactory
66+
67+
68+
pytest_generate_tests = pytest_generate_tests_for_array_contexts([
69+
PytestPyOpenCLArrayContextFactory,
70+
])
71+
72+
73+
def to_scalar(val):
74+
"""Convert symbolic or array value to scalar."""
75+
if hasattr(val, "evalf"):
76+
val = val.evalf()
77+
if hasattr(val, "item"):
78+
val = val.item()
79+
return complex(val)
80+
81+
82+
class NumericMatVecOperator:
83+
"""Wrapper for symbolic matrix-vector operator with numeric
84+
substitution."""
85+
86+
def __init__(self, symbolic_op, repl_dict):
87+
self.symbolic_op = symbolic_op
88+
self.repl_dict = repl_dict
89+
self.shape = symbolic_op.shape
90+
91+
def matvec(self, vec):
92+
result = self.symbolic_op.matvec(vec)
93+
numeric_result = []
94+
for expr in result:
95+
if hasattr(expr, "xreplace"):
96+
numeric_result.append(
97+
complex(expr.xreplace(self.repl_dict).evalf())
98+
)
99+
else:
100+
numeric_result.append(complex(expr))
101+
return np.array(numeric_result)
102+
103+
104+
def get_repl_dict(kernel, extra_kwargs):
105+
"""Numeric substitution for symbolic kernel parameters."""
106+
repl_dict = {}
107+
if "lam" in extra_kwargs:
108+
repl_dict[sp.Symbol("lam")] = extra_kwargs["lam"]
109+
if "k" in extra_kwargs:
110+
repl_dict[sp.Symbol("k")] = extra_kwargs["k"]
111+
return repl_dict
112+
113+
114+
@pytest.mark.parametrize("knl,extra_kwargs", [
115+
(LaplaceKernel(2), {}),
116+
(YukawaKernel(2), {"lam": 0.1}),
117+
(HelmholtzKernel(2), {"k": 0.5}),
118+
(BiharmonicKernel(2), {}),
119+
])
120+
def test_l2l_coefficient_differences(
121+
actx_factory: ArrayContextFactory,
122+
knl: Kernel,
123+
extra_kwargs: Mapping[str, float],
124+
verbose: bool = True,
125+
):
126+
"""
127+
Test L2L coefficient differences between formula and direct
128+
computation.
129+
"""
130+
order = 7
131+
dim = 2
132+
repl_dict = get_repl_dict(knl, extra_kwargs)
133+
134+
# Setup sources and centers
135+
source = np.array([[5.0], [5.0]])
136+
c1 = np.array([0.0, 0.0])
137+
c2 = c1 + np.array([-0.5, 1.0])
138+
strength = np.array([1.0])
139+
140+
actx = actx_factory()
141+
toy_ctx = t.ToyContext(
142+
knl,
143+
local_expn_class=LinearPDEConformingVolumeTaylorLocalExpansion,
144+
extra_kernel_kwargs=extra_kwargs
145+
)
146+
toy_ctx_full = t.ToyContext(
147+
knl,
148+
local_expn_class=VolumeTaylorLocalExpansion,
149+
extra_kernel_kwargs=extra_kwargs
150+
)
151+
152+
# Compute expansions
153+
p = t.PointSources(toy_ctx, source, weights=strength)
154+
p_full = t.PointSources(toy_ctx_full, source, weights=strength)
155+
156+
p2l = t.local_expand(actx, p, c1, order=order, rscale=1.0)
157+
p2l2l = t.local_expand(actx, p2l, c2, order=order, rscale=1.0)
158+
p2l_full = t.local_expand(actx, p_full, c1, order=order, rscale=1.0)
159+
p2l2l_full = t.local_expand(actx, p2l_full, c2, order=order, rscale=1.0)
160+
161+
# Build matrix M
162+
p2l2l_expn = LinearPDEConformingVolumeTaylorLocalExpansion(knl, order)
163+
wrangler = p2l2l_expn.expansion_terms_wrangler
164+
M_symbolic = wrangler.get_projection_matrix(rscale=1.0) # noqa: N806
165+
numeric_op = NumericMatVecOperator(M_symbolic, repl_dict)
166+
M = build_matrix(numeric_op, dtype=np.complex128) # noqa: N806
167+
168+
# Get compressed coefficients
169+
mu_c_symbolic = wrangler.get_full_kernel_derivatives_from_stored(
170+
p2l2l.coeffs, rscale=1.0
171+
)
172+
mu_c = []
173+
for coeff in mu_c_symbolic:
174+
if hasattr(coeff, "xreplace"):
175+
mu_c.append(to_scalar(coeff.xreplace(repl_dict)))
176+
else:
177+
mu_c.append(to_scalar(coeff))
178+
179+
# Get identifiers
180+
stored_identifiers = p2l2l_expn.get_coefficient_identifiers()
181+
full_identifiers = p2l2l_expn.get_full_coefficient_identifiers()
182+
lexpn = VolumeTaylorLocalExpansion(knl, order)
183+
lexpn_idx = lexpn.get_full_coefficient_identifiers()
184+
185+
h = c2 - c1
186+
global_const = to_scalar(knl.get_global_scaling_const())
187+
188+
if verbose:
189+
print(f'\n{"="*104}')
190+
print(f"L2L Verification: {type(knl).__name__} (order={order})")
191+
print(f'{"="*104}')
192+
print(f"c1 = {c1}, c2 = {c2}, h = {h}")
193+
print()
194+
print(f"{'i':>3s} | {'ν(i)':>15s} | {'|ν|':4s} | " # noqa: RUF001
195+
f"{'formula':>31s} | {'direct':>31s} | {'abs err':>10s}")
196+
print("-" * 104)
197+
198+
max_abs_error = 0.0
199+
200+
for i, nu_i in enumerate(full_identifiers):
201+
i_card = sum(np.array(nu_i))
202+
203+
# Compute error by formula
204+
error = 0.0 + 0.0j
205+
for k, nu_jk in enumerate(stored_identifiers):
206+
jk_card = sum(np.array(nu_jk))
207+
if jk_card >= i_card:
208+
continue
209+
210+
start_idx = math.comb(order - i_card + dim, dim)
211+
end_idx = math.comb(order - jk_card + dim, dim)
212+
213+
for q_idx in range(start_idx, end_idx):
214+
nu_q = full_identifiers[q_idx]
215+
nu_sum = tuple(map(sum, zip(nu_q, nu_jk, strict=True)))
216+
if nu_sum not in full_identifiers:
217+
continue
218+
219+
deriv_idx = full_identifiers.index(nu_sum)
220+
gamma_deriv = to_scalar(p2l_full.coeffs[deriv_idx])
221+
h_pow = np.prod(h ** np.array(nu_q))
222+
fact_nu_q = np.prod(spsp.factorial(nu_q))
223+
224+
error += -M[i, k] * gamma_deriv * h_pow / fact_nu_q
225+
226+
error /= np.prod(spsp.factorial(nu_i))
227+
error *= global_const
228+
229+
# Compute direct difference
230+
true_i_idx = lexpn_idx.index(nu_i)
231+
mu_full = to_scalar(p2l2l_full.coeffs[true_i_idx])
232+
direct_diff = (mu_full - mu_c[i]) / np.prod(spsp.factorial(nu_i))
233+
direct_diff *= global_const
234+
235+
# Compute errors
236+
abs_err = abs(error - direct_diff)
237+
max_abs_error = max(max_abs_error, abs_err)
238+
239+
if verbose:
240+
print(f"{i:3d} | {nu_i!s:>15s} | {i_card:4d} | "
241+
f"{error.real: .8e}{error.imag:+.8e}j | "
242+
f"{direct_diff.real: .8e}{direct_diff.imag:+.8e}j | "
243+
f"{abs_err:9.2e}")
244+
245+
if verbose:
246+
print(f"\nMaximum absolute error: {max_abs_error:.2e}")
247+
248+
assert max_abs_error < 1e-10, \
249+
f"{type(knl).__name__}: error {max_abs_error:.2e}"
250+
251+
252+
if __name__ == "__main__":
253+
if len(sys.argv) > 1:
254+
exec(sys.argv[1])
255+
else:
256+
pytest.main([__file__])

0 commit comments

Comments
 (0)