1+ """
2+ Verification for the difference between compressed L2L coefficients and
3+ full L2L coefficients.
4+ """
5+ import os
6+ os .environ ['PYOPENCL_CTX' ] = '0'
7+
8+ import math
9+ import numpy as np
10+ import scipy .special as spsp
11+ import sympy as sp
12+ import pytest
13+ import pyopencl as cl
14+ import sumpy .toys as t
15+ from sumpy .kernel import (
16+ YukawaKernel ,
17+ HelmholtzKernel ,
18+ LaplaceKernel ,
19+ BiharmonicKernel
20+ )
21+ from sumpy .expansion .local import (
22+ LinearPDEConformingVolumeTaylorLocalExpansion ,
23+ VolumeTaylorLocalExpansion
24+ )
25+ from sumpy .array_context import _acf
26+ from sumpy .tools import build_matrix
27+
28+
29+ VERBOSE = False
30+
31+ def to_scalar (val ):
32+ """Convert symbolic or array value to scalar."""
33+ if hasattr (val , 'evalf' ):
34+ val = val .evalf ()
35+ if hasattr (val , 'item' ):
36+ val = val .item ()
37+ return complex (val )
38+
39+
40+ class NumericMatVecOperator :
41+ """Wrapper for symbolic matrix-vector operator with numeric
42+ substitution."""
43+
44+ def __init__ (self , symbolic_op , repl_dict ):
45+ self .symbolic_op = symbolic_op
46+ self .repl_dict = repl_dict
47+ self .shape = symbolic_op .shape
48+
49+ def matvec (self , vec ):
50+ result = self .symbolic_op .matvec (vec )
51+ numeric_result = []
52+ for expr in result :
53+ if hasattr (expr , 'xreplace' ):
54+ numeric_result .append (
55+ complex (expr .xreplace (self .repl_dict ).evalf ())
56+ )
57+ else :
58+ numeric_result .append (complex (expr ))
59+ return np .array (numeric_result )
60+
61+
62+ def get_repl_dict (kernel , extra_kwargs ):
63+ """Numeric substitution for symbolic kernel parameters."""
64+ repl_dict = {}
65+ if 'lam' in extra_kwargs :
66+ repl_dict [sp .Symbol ('lam' )] = extra_kwargs ['lam' ]
67+ if 'k' in extra_kwargs :
68+ repl_dict [sp .Symbol ('k' )] = extra_kwargs ['k' ]
69+ return repl_dict
70+
71+
72+ @pytest .mark .parametrize ("knl,extra_kwargs" , [
73+ (LaplaceKernel (2 ), {}),
74+ (YukawaKernel (2 ), {'lam' : 0.1 }),
75+ (HelmholtzKernel (2 ), {'k' : 0.5 }),
76+ (BiharmonicKernel (2 ), {}),
77+ ])
78+
79+ def test_l2l_coefficient_differences (knl , extra_kwargs ):
80+ """
81+ Test L2L coefficient differences between formula and direct
82+ computation.
83+ """
84+ order = 7
85+ dim = 2
86+ repl_dict = get_repl_dict (knl , extra_kwargs )
87+
88+ # Setup sources and centers
89+ source = np .array ([[5.0 ], [5.0 ]])
90+ c1 = np .array ([0.0 , 0.0 ])
91+ c2 = c1 + np .array ([- 0.5 , 1.0 ])
92+ strength = np .array ([1.0 ])
93+
94+ actx = _acf ()
95+ toy_ctx = t .ToyContext (
96+ knl ,
97+ local_expn_class = LinearPDEConformingVolumeTaylorLocalExpansion ,
98+ extra_kernel_kwargs = extra_kwargs
99+ )
100+ toy_ctx_full = t .ToyContext (
101+ knl ,
102+ local_expn_class = VolumeTaylorLocalExpansion ,
103+ extra_kernel_kwargs = extra_kwargs
104+ )
105+
106+ # Compute expansions
107+ p = t .PointSources (toy_ctx , source , weights = strength )
108+ p_full = t .PointSources (toy_ctx_full , source , weights = strength )
109+
110+ p2l = t .local_expand (actx , p , c1 , order = order , rscale = 1.0 )
111+ p2l2l = t .local_expand (actx , p2l , c2 , order = order , rscale = 1.0 )
112+ p2l_full = t .local_expand (actx , p_full , c1 , order = order , rscale = 1.0 )
113+ p2l2l_full = t .local_expand (actx , p2l_full , c2 , order = order , rscale = 1.0 )
114+
115+ # Build matrix M
116+ p2l2l_expn = LinearPDEConformingVolumeTaylorLocalExpansion (knl , order )
117+ wrangler = p2l2l_expn .expansion_terms_wrangler
118+ M_symbolic = wrangler .get_projection_matrix (rscale = 1.0 )
119+ numeric_op = NumericMatVecOperator (M_symbolic , repl_dict )
120+ M = build_matrix (numeric_op , dtype = np .complex128 )
121+
122+ # Get compressed coefficients
123+ mu_c_symbolic = wrangler .get_full_kernel_derivatives_from_stored (
124+ p2l2l .coeffs , rscale = 1.0
125+ )
126+ mu_c = []
127+ for coeff in mu_c_symbolic :
128+ if hasattr (coeff , 'xreplace' ):
129+ mu_c .append (to_scalar (coeff .xreplace (repl_dict )))
130+ else :
131+ mu_c .append (to_scalar (coeff ))
132+
133+ # Get identifiers
134+ stored_identifiers = p2l2l_expn .get_coefficient_identifiers ()
135+ full_identifiers = p2l2l_expn .get_full_coefficient_identifiers ()
136+ lexpn_idx = VolumeTaylorLocalExpansion (knl , order ).get_full_coefficient_identifiers ()
137+
138+ h = c2 - c1
139+ global_const = to_scalar (knl .get_global_scaling_const ())
140+
141+ if VERBOSE :
142+ print (f'\n { "=" * 104 } ' )
143+ print (f'L2L Verification: { type (knl ).__name__ } (order={ order } )' )
144+ print (f'{ "=" * 104 } ' )
145+ print (f'c1 = { c1 } , c2 = { c2 } , h = { h } ' )
146+ print ()
147+ print (f"{ 'i' :>3s} | { 'ν(i)' :>15s} | { '|ν|' :4s} | "
148+ f"{ 'formula' :>31s} | { 'direct' :>31s} | { 'abs err' :>10s} " )
149+ print ("-" * 104 )
150+
151+ max_abs_error = 0.0
152+
153+ for i , nu_i in enumerate (full_identifiers ):
154+ i_card = sum (np .array (nu_i ))
155+
156+ # Compute error by formula
157+ error = 0.0 + 0.0j
158+ for k , nu_jk in enumerate (stored_identifiers ):
159+ jk_card = sum (np .array (nu_jk ))
160+ if jk_card >= i_card :
161+ continue
162+
163+ start_idx = math .comb (order - i_card + dim , dim )
164+ end_idx = math .comb (order - jk_card + dim , dim )
165+
166+ for q_idx in range (start_idx , end_idx ):
167+ nu_q = full_identifiers [q_idx ]
168+ nu_sum = tuple (map (sum , zip (nu_q , nu_jk )))
169+ if nu_sum not in full_identifiers :
170+ continue
171+
172+ deriv_idx = full_identifiers .index (nu_sum )
173+ gamma_deriv = to_scalar (p2l_full .coeffs [deriv_idx ])
174+ h_pow = np .prod (h ** np .array (nu_q ))
175+ fact_nu_q = np .prod (spsp .factorial (nu_q ))
176+
177+ error += - M [i , k ] * gamma_deriv * h_pow / fact_nu_q
178+
179+ error /= np .prod (spsp .factorial (nu_i ))
180+ error *= global_const
181+
182+ # Compute direct difference
183+ true_i_idx = lexpn_idx .index (nu_i )
184+ mu_full = to_scalar (p2l2l_full .coeffs [true_i_idx ])
185+ direct_diff = (mu_full - mu_c [i ]) / np .prod (spsp .factorial (nu_i ))
186+ direct_diff *= global_const
187+
188+ # Compute errors
189+ abs_err = abs (error - direct_diff )
190+ max_abs_error = max (max_abs_error , abs_err )
191+
192+ if VERBOSE :
193+ print (f"{ i :3d} | { str (nu_i ):>15s} | { i_card :4d} | "
194+ f"{ error .real : .8e} { error .imag :+.8e} j | "
195+ f"{ direct_diff .real : .8e} { direct_diff .imag :+.8e} j | "
196+ f"{ abs_err :9.2e} " )
197+
198+ if VERBOSE :
199+ print (f"\n Maximum absolute error: { max_abs_error :.2e} " )
200+ print (f'{ "=" * 104 } \n ' )
201+
202+ assert max_abs_error < 1e-10 , \
203+ f"{ type (knl ).__name__ } : error { max_abs_error :.2e} "
204+
205+
206+ if __name__ == "__main__" :
207+ pytest .main ([__file__ , "-v" , "-s" ])
0 commit comments