Skip to content

Commit e4b6319

Browse files
committed
test for m2m
1 parent d275153 commit e4b6319

1 file changed

Lines changed: 233 additions & 0 deletions

File tree

sumpy/test/test_m2m.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
"""
2+
Verification of compressed M2M translation.
3+
4+
Compares two approaches: 1. Compress coefficients -> embed -> M2M translate
5+
2. M2M translate with full coefficients
6+
"""
7+
from __future__ import annotations
8+
9+
import os
10+
11+
12+
os.environ["PYOPENCL_CTX"] = "0"
13+
14+
import math
15+
16+
import numpy as np
17+
import pytest
18+
import scipy.special as spsp
19+
import sympy as sp
20+
21+
import sumpy.toys as t
22+
from sumpy.array_context import _acf
23+
from sumpy.expansion.local import (
24+
LinearPDEConformingVolumeTaylorLocalExpansion,
25+
)
26+
from sumpy.expansion.multipole import (
27+
LinearPDEConformingVolumeTaylorMultipoleExpansion,
28+
VolumeTaylorMultipoleExpansion,
29+
)
30+
from sumpy.kernel import (
31+
BiharmonicKernel,
32+
HelmholtzKernel,
33+
LaplaceKernel,
34+
YukawaKernel,
35+
)
36+
from sumpy.tools import build_matrix
37+
38+
39+
VERBOSE = False
40+
41+
42+
def to_scalar(val):
43+
"""Convert symbolic or array value to scalar."""
44+
if hasattr(val, "evalf"):
45+
val = val.evalf()
46+
if hasattr(val, "item"):
47+
val = val.item()
48+
return complex(val)
49+
50+
51+
class NumericMatVecOperator:
52+
"""Wrapper for symbolic matrix-vector operator with numeric
53+
substitution."""
54+
55+
def __init__(self, symbolic_op, repl_dict):
56+
self.symbolic_op = symbolic_op
57+
self.repl_dict = repl_dict
58+
self.shape = symbolic_op.shape
59+
60+
def matvec(self, vec):
61+
result = self.symbolic_op.matvec(vec)
62+
out = []
63+
for expr in result:
64+
if hasattr(expr, "xreplace"):
65+
out.append(complex(expr.xreplace(self.repl_dict).evalf()))
66+
else:
67+
out.append(complex(expr))
68+
return np.array(out)
69+
70+
71+
def get_repl_dict(kernel, extra_kwargs):
72+
"""Numeric substitution for symbolic kernel parameters."""
73+
repl_dict = {}
74+
if "lam" in extra_kwargs:
75+
repl_dict[sp.Symbol("lam")] = extra_kwargs["lam"]
76+
if "k" in extra_kwargs:
77+
repl_dict[sp.Symbol("k")] = extra_kwargs["k"]
78+
return repl_dict
79+
80+
81+
@pytest.mark.parametrize("knl,extra_kwargs", [
82+
(LaplaceKernel(2), {}),
83+
(YukawaKernel(2), {"lam": 0.1}),
84+
(HelmholtzKernel(2), {"k": 0.5}),
85+
(BiharmonicKernel(2), {}),
86+
])
87+
def test_m2m(knl, extra_kwargs):
88+
"""Verify M2M errors by comparing formula vs direct computation."""
89+
order = 7
90+
dim = 2
91+
repl_dict = get_repl_dict(knl, extra_kwargs)
92+
global_const = to_scalar(knl.get_global_scaling_const())
93+
94+
# Set up source, centers, and target
95+
source = np.array([[0.0], [0.1]])
96+
strength = np.array([1.0])
97+
98+
m_center1 = np.array([0.0, 0.0])
99+
offset_direction = np.array([-0.5, 0.25])
100+
c2_c1_dist = 0.1
101+
m_center2 = m_center1 + c2_c1_dist * offset_direction
102+
h = m_center2 - m_center1
103+
104+
target = np.array([[2.0], [2.0]])
105+
106+
if VERBOSE:
107+
print(f"M2M Coefficient Verification for {type(knl).__name__}:")
108+
print(f"m_center1 = {m_center1}")
109+
print(f"m_center2 = {m_center2}")
110+
print(f"h = m_center2 - m_center1 = {h}")
111+
print()
112+
print(f"{'k':>3s} | {'ν(k)':>15s} | {'|ν(k)|':6s} | " # noqa: RUF001
113+
f"{'difference by formula':>31s} | "
114+
f"{'difference by direct computation':>31s} | "
115+
f"{'abs err':>10s}")
116+
print("-" * 120)
117+
118+
actx = _acf()
119+
120+
toy_ctx_full = t.ToyContext(
121+
knl,
122+
mpole_expn_class=VolumeTaylorMultipoleExpansion,
123+
extra_kernel_kwargs=extra_kwargs
124+
)
125+
126+
toy_ctx_local = t.ToyContext(
127+
knl,
128+
local_expn_class=LinearPDEConformingVolumeTaylorLocalExpansion,
129+
extra_kernel_kwargs=extra_kwargs
130+
)
131+
132+
p_full = t.PointSources(toy_ctx_full, source, weights=strength)
133+
p2m_full = t.multipole_expand(actx, p_full, m_center1, order=order, rscale=1.0)
134+
135+
p_local = t.PointSources(toy_ctx_local, m_center2.reshape(2, 1), weights=strength)
136+
p2l = t.local_expand(actx, p_local, target, order=order)
137+
138+
mexpn = LinearPDEConformingVolumeTaylorMultipoleExpansion(knl, order)
139+
140+
# Build matrix M
141+
wrangler = mexpn.expansion_terms_wrangler
142+
M_symbolic = wrangler.get_projection_matrix(rscale=1.0) # noqa: N806
143+
numeric_op = NumericMatVecOperator(M_symbolic, repl_dict)
144+
M = build_matrix(numeric_op, dtype=np.complex128) # noqa: N806
145+
coeffs_full = (M @ p2l.coeffs) * global_const
146+
147+
# Get coefficient identifiers
148+
stored_identifiers = mexpn.get_coefficient_identifiers()
149+
full_identifiers = mexpn.get_full_coefficient_identifiers()
150+
is_stored = [mi in stored_identifiers for mi in full_identifiers]
151+
stored_indices = [i for i, st in enumerate(is_stored) if st]
152+
153+
mexpn_full = VolumeTaylorMultipoleExpansion(knl, order)
154+
mexpn_full_idx = mexpn_full.get_full_coefficient_identifiers()
155+
156+
max_abs_error = 0.0
157+
158+
for k, nu_k in enumerate(full_identifiers):
159+
k_card = sum(np.array(nu_k))
160+
# assume all coefficient values are 1
161+
alpha_k = 1
162+
163+
true_k_idx = mexpn_full_idx.index(nu_k)
164+
basis_full = np.zeros(len(mexpn_full_idx), dtype=np.complex128)
165+
basis_full[true_k_idx] = alpha_k
166+
p2m_full_k = p2m_full.with_coeffs(basis_full)
167+
168+
# M^T @ alpha
169+
basis_cmp = np.zeros(M.shape[0], dtype=np.complex128)
170+
basis_cmp[stored_indices] = M[k, :] * alpha_k
171+
172+
# Embed back into full basis
173+
basis_cmp_full = np.zeros(len(mexpn_full_idx), dtype=np.complex128)
174+
for i, nu_i in enumerate(full_identifiers):
175+
if basis_cmp[i] != 0:
176+
true_i_idx = mexpn_full_idx.index(nu_i)
177+
basis_cmp_full[true_i_idx] = basis_cmp[i]
178+
179+
p2m_cmp_k = p2m_full.with_coeffs(basis_cmp_full)
180+
181+
p2m2m_cmp = t.multipole_expand(
182+
actx, p2m_cmp_k, m_center2, order=order
183+
).eval(actx, target)
184+
p2m2m_full = t.multipole_expand(
185+
actx, p2m_full_k, m_center2, order=order
186+
).eval(actx, target)
187+
188+
direct_diff = (p2m2m_cmp - p2m2m_full)[0]
189+
190+
error = 0.0 + 0.0j
191+
for s, nu_js in enumerate(stored_identifiers):
192+
nu_js_card = sum(np.array(nu_js))
193+
inner_sum = 0.0 + 0.0j
194+
195+
if nu_js_card <= k_card:
196+
start_idx = math.comb(order - k_card + dim, dim)
197+
end_idx = math.comb(order - nu_js_card + dim, dim)
198+
199+
for idx in range(start_idx, end_idx):
200+
nu_l = full_identifiers[idx]
201+
nu_sum = tuple(a + b for a, b in zip(nu_l, nu_js, strict=True))
202+
203+
if nu_sum not in full_identifiers:
204+
continue
205+
206+
derivative_idx = full_identifiers.index(nu_sum)
207+
h_pow = np.prod(h ** np.array(nu_l))
208+
fact_nu_l = np.prod(spsp.factorial(nu_l))
209+
210+
inner_sum += coeffs_full[derivative_idx] * h_pow / fact_nu_l
211+
212+
error += inner_sum * M[k, s]
213+
214+
abs_err = abs(error - direct_diff)
215+
max_abs_error = max(max_abs_error, abs_err)
216+
217+
if VERBOSE:
218+
print(f"{k:3d} | {nu_k!s:>15s} | {k_card:6d} | "
219+
f"{error.real: .8e}{error.imag:+.8e}j | "
220+
f"{direct_diff.real: .8e}{direct_diff.imag:+.8e}j | "
221+
f"{abs_err:9.2e}")
222+
223+
if VERBOSE:
224+
print(f"\nMaximum absolute error: {max_abs_error:.2e}")
225+
226+
assert max_abs_error < 1e-15, (
227+
f"{type(knl).__name__}: error {max_abs_error:.2e}"
228+
)
229+
230+
231+
if __name__ == "__main__":
232+
os.environ.setdefault("PYOPENCL_CTX", "0")
233+
pytest.main([__file__, "-v", "-s"])

0 commit comments

Comments
 (0)