1+ import os
2+ from functools import partial
3+ from typing import Tuple
4+
5+ import numpy as np
6+ import pytest
7+ from jax import device_put , devices , jit , grad , value_and_grad
8+ from jax import numpy as jnp
9+ from matplotlib import pyplot as plt
10+ from scipy .io import loadmat , savemat
11+
12+ from jwave import FourierSeries
13+ from jwave .acoustics .time_harmonic import helmholtz_solver
14+ from jwave .geometry import Domain , Medium
15+ from jwave .utils import plot_comparison
16+
17+ RELATIVE_TOLERANCE = 1e-4
18+ DIR_PATH = os .path .dirname (os .path .realpath (__file__ ))
19+
20+ def _make_filename (N , dx , sound_speed , density , attenuation , omega ):
21+ N = str (N ).replace (" " , "_" )
22+ return os .path .join (
23+ DIR_PATH ,
24+ ".." ,
25+ "regression_data" ,
26+ f"helmholtz_autodiff_{ N } _{ dx } _{ sound_speed } _{ density } _{ attenuation } _{ omega } .mat" .replace (" " , "_" )
27+ )
28+
29+
30+ def _index_in_middle (N , span = 7 ):
31+ return tuple (slice (Ni // 2 - span , Ni // 2 + span ) for Ni in N )
32+
33+
34+ def _get_sos (kind , domain ):
35+ match kind :
36+ case "scalar" :
37+ return 1500.
38+ case "homogeneous" :
39+ return FourierSeries (np .ones (domain .N ) * 1500. , domain )
40+ case "heterogeneous" :
41+ c = np .ones (domain .N ) * 1500.
42+ c [_index_in_middle (domain .N )] = 2000.
43+ return FourierSeries (c , domain )
44+
45+
46+ def _get_density (kind , domain ):
47+ match kind :
48+ case "scalar" :
49+ return 1000.
50+ case "homogeneous" :
51+ return FourierSeries (np .ones (domain .N ) * 1000. , domain )
52+ case "heterogeneous" :
53+ rho = np .ones (domain .N ) * 1000.
54+ rho [_index_in_middle (domain .N )] = 2000.
55+ return FourierSeries (rho , domain )
56+
57+
58+ def _get_attenuation (kind , domain ):
59+ match kind :
60+ case "scalar" :
61+ return 0.1
62+ case "homogeneous" :
63+ return FourierSeries (np .zeros (domain .N ), domain )
64+ case "heterogeneous" :
65+ alpha = np .zeros (domain .N )
66+ alpha [_index_in_middle (domain .N )] = 10.
67+ return FourierSeries (alpha , domain )
68+
69+
70+ @pytest .mark .parametrize ("N" , [(48 ,48 ), (49 ,47 ), (32 ,32 ,32 ), (33 ,31 ,32 )])
71+ @pytest .mark .parametrize ("dx" , [1e-3 ])
72+ @pytest .mark .parametrize ("sound_speed" , ["heterogeneous" ])
73+ @pytest .mark .parametrize ("density" , ["heterogeneous" ])
74+ @pytest .mark .parametrize ("attenuation" , ["heterogeneous" ])
75+ @pytest .mark .parametrize ("omega" , [1e6 ])
76+ def test_regression_helmholtz (
77+ N , dx , sound_speed , density , attenuation , omega , reset_regression_data = False
78+ ):
79+ # Setting up simulation
80+ dx = tuple ([dx ] * len (N ))
81+ domain = Domain (N , dx )
82+ filename = _make_filename (N , dx , sound_speed , density , attenuation , omega )
83+
84+ # Making source map (dirac at center of domain)
85+ src = jnp .zeros (N , dtype = jnp .complex64 )
86+ src = src .at [tuple (11 for Ni in N )].set (1. )
87+ src = FourierSeries (src , domain )
88+
89+ # Making medium
90+ sound_speed = _get_sos (sound_speed , domain )
91+ density = _get_density (density , domain )
92+ attenuation = _get_attenuation (attenuation , domain )
93+
94+ # Move everythin to cpu
95+ cpu = devices ("cpu" )[0 ]
96+ src = device_put (src , cpu )
97+ sound_speed = device_put (sound_speed , cpu )
98+ density = device_put (density , cpu )
99+ attenuation = device_put (attenuation , cpu )
100+
101+ @jit
102+ @partial (grad , argnums = [0 ,1 ,2 ,3 ,4 ], has_aux = True )
103+ def loss_fn (
104+ sound_speed : FourierSeries ,
105+ density : FourierSeries ,
106+ attenuation : FourierSeries ,
107+ omega : float ,
108+ src : FourierSeries ,
109+ ):
110+ # This tries to maximize the amplitude of the field at a given point
111+ medium = Medium (src .domain , sound_speed , density , attenuation , pml_size = 10 )
112+ solution_field = helmholtz_solver (medium , omega , src , tol = 1e-5 ).on_grid
113+ max_point = [- 11 ] * len (src .domain .N )
114+ max_point = tuple (max_point )
115+ return - jnp .sum (jnp .abs (solution_field [max_point ])), solution_field
116+
117+ # Get gradients
118+ gradients , field = loss_fn (
119+ sound_speed ,
120+ density ,
121+ attenuation ,
122+ omega ,
123+ src
124+ )
125+
126+ # Make them numpy arrays
127+ sos_gradient = np .array (gradients [0 ].on_grid )
128+ density_gradient = np .array (gradients [1 ].on_grid )
129+ attenuation_gradient = np .array (gradients [2 ].on_grid )
130+ omega_gradient = np .array (gradients [3 ])
131+ src_gradient = np .array (gradients [4 ].on_grid )
132+
133+ # Reset regression data if needed
134+ if reset_regression_data :
135+ field = np .array (field )
136+ savemat (filename , {
137+ "sos_gradient" : sos_gradient ,
138+ "density_gradient" : density_gradient ,
139+ "attenuation_gradient" : attenuation_gradient ,
140+ "omega_gradient" : omega_gradient ,
141+ "src_gradient" : src_gradient ,
142+ "field" : field
143+ })
144+
145+ # Load regression data
146+ matfile = loadmat (filename )
147+
148+ # Check each one of them
149+ err_fun = lambda x ,y : jnp .amax (jnp .abs (x - y ))/ jnp .amax (jnp .abs (y ))
150+ max_rel_error = max ([
151+ err_fun (sos_gradient , matfile ["sos_gradient" ]),
152+ err_fun (density_gradient , matfile ["density_gradient" ]),
153+ err_fun (attenuation_gradient , matfile ["attenuation_gradient" ]),
154+ err_fun (omega_gradient , matfile ["omega_gradient" ]),
155+ err_fun (src_gradient , matfile ["src_gradient" ]),
156+ ])
157+
158+ # Make sure the solution is the same within a certain tolerance
159+ print (" Relative max error = " , 100 * max_rel_error , "%" )
160+
161+ assert max_rel_error < RELATIVE_TOLERANCE , (
162+ "Test failed, error above maximum limit of "
163+ + str (100 * RELATIVE_TOLERANCE )
164+ + "%"
165+ )
166+
0 commit comments