Coverage for kwave/kspaceFirstOrder2D.py: 16%
74 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 11:55 -0700
« prev ^ index » next coverage.py v6.5.0, created at 2022-10-24 11:55 -0700
1from numpy.fft import ifftshift
2import tempfile
4from kwave.executor import Executor
5from kwave.kWaveSimulation import kWaveSimulation
6from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func
7from kwave.kspaceFirstOrder import *
8from kwave.utils import *
11@kspaceFirstOrderG
12def kspaceFirstOrder2DG(**kwargs):
13 """
14 2D ime-domain simulation of wave propagation on a GPU using C++ CUDA code.
16 kspaceFirstOrder2DG provides a blind interface to the C++/CUDA
17 version of kspaceFirstOrder2D (called kspaceFirstOrder-CUDA) in the
18 same way as kspaceFirstOrder3DC. Note, the C++ code does not support
19 all input options, and all display options are ignored (only command
20 line outputs are given). See the k-Wave user manual for more
21 information.
22 The function works by appending the optional input 'SaveToDisk' to
23 the user inputs and then calling kspaceFirstOrder2D to save the input
24 files to disk. The contents of sensor.record (if set) are parsed as
25 input flags, and the C++ code is run using the system command. The
26 output files are then automatically loaded from disk and returned in
27 the same fashion as kspaceFirstOrder2D. The input and output files
28 are saved to the temporary directory native to the operating system,
29 and are deleted after the function runs.
30 This function requires the C++ binary/executable of
31 kspaceFirstOrder-CUDA to be downloaded from
32 http://www.k-wave.org/download.php and placed in the "binaries"
33 directory of the k-Wave toolbox (the 2D and 3D code use the same
34 binary). Alternatively, the name and location of the binary can be
35 specified using the optional input parameters 'BinaryName' and
36 'BinariesPath'.
38 This function is essentially a wrapper and directly uses the capabilities
39 of kspaceFirstOrder3DC by replacing the binary name with the name of the
40 GPU binary.
41 """
42 sensor_data = kspaceFirstOrder2DC(**kwargs) # pass inputs to CPU version
43 return sensor_data
46@kspaceFirstOrderC()
47def kspaceFirstOrder2DC(**kwargs):
48 """
49 2D time-domain simulation of wave propagation using C++ code.
51 kspaceFirstOrder2DC provides a blind interface to the C++ version of
52 kspaceFirstOrder2D (called kspaceFirstOrder-OMP) in the same way as
53 kspaceFirstOrder3DC. Note, the C++ code does not support all input
54 options, and all display options are ignored (only command line
55 outputs are given). See the k-Wave user manual for more information.
56 The function works by appending the optional input 'SaveToDisk' to
57 the user inputs and then calling kspaceFirstOrder2D to save the input
58 files to disk. The contents of sensor.record (if set) are parsed as
59 input flags, and the C++ code is run using the system command. The
60 output files are then automatically loaded from disk and returned in
61 the same fashion as kspaceFirstOrder2D. The input and output files
62 are saved to the temporary directory native to the operating system,
63 and are deleted after the function runs.
64 For small simulations, running the simulation on a smaller number of
65 cores can improve performance as the matrices are often small enough
66 to fit within cache. It is recommended to adjust the value of
67 'NumThreads' to optimise performance for a given simulation size and
68 computer hardware. By default, simulations smaller than 128^2 are
69 set to run using a single thread (this behaviour can be over-ridden
70 using the 'NumThreads' option). In some circumstances, for very small
71 simulations, the C++ code can be slower than the MATLAB code.
72 This function requires the C++ binary/executable of
73 kspaceFirstOrder-OMP to be downloaded from
74 http://www.k-wave.org/download.php and placed in the "binaries"
75 directory of the k-Wave toolbox (the same binary is used for
76 simulations in 2D, 3D, and axisymmetric coordinates). Alternatively,
77 the name and location of the binary can be specified using the
78 optional input parameters 'BinaryName' and 'BinariesPath'.
80 This function is essentially a wrapper and directly uses the capabilities
81 of kspaceFirstOrder3DC by replacing the binary name with the name of the
82 GPU binary.
84 Args:
85 **kwargs:
87 Returns:
89 """
90 # generate the input file and save to disk
91 kspaceFirstOrder2D(**kwargs)
92 return kwargs['SaveToDisk']
95def kspaceFirstOrder2D(kgrid, medium, source, sensor, **kwargs):
96 """
97 2D time-domain simulation of wave propagation.
99 kspaceFirstOrder2D simulates the time-domain propagation of
100 compressional waves through a two-dimensional homogeneous or
101 heterogeneous acoustic medium given four input structures: kgrid,
102 medium, source, and sensor. The computation is based on a first-order
103 k-space model which accounts for power law absorption and a
104 heterogeneous sound speed and density. If medium.BonA is specified,
105 cumulative nonlinear effects are also modelled. At each time-step
106 (defined by kgrid.dt and kgrid.Nt or kgrid.t_array), the acoustic
107 field parameters at the positions defined by sensor.mask are recorded
108 and stored. If kgrid.t_array is set to 'auto', this array is
109 automatically generated using the makeTime method of the kWaveGrid
110 class. An anisotropic absorbing boundary layer called a perfectly
111 matched layer (PML) is implemented to prevent waves that leave one
112 side of the domain being reintroduced from the opposite side (a
113 consequence of using the FFT to compute the spatial derivatives in
114 the wave equation). This allows infinite domain simulations to be
115 computed using small computational grids.
117 For a homogeneous medium the formulation is exact and the time-steps
118 are only limited by the effectiveness of the perfectly matched layer.
119 For a heterogeneous medium, the solution represents a leap-frog
120 pseudospectral method with a k-space correction that improves the
121 accuracy of computing the temporal derivatives. This allows larger
122 time-steps to be taken for the same level of accuracy compared to
123 conventional pseudospectral time-domain methods. The computational
124 grids are staggered both spatially and temporally.
126 An initial pressure distribution can be specified by assigning a
127 matrix (the same size as the computational grid) of arbitrary numeric
128 values to source.p0. A time varying pressure source can similarly be
129 specified by assigning a binary matrix (i.e., a matrix of 1's and 0's
130 with the same dimensions as the computational grid) to source.p_mask
131 where the 1's represent the grid points that form part of the source.
132 The time varying input signals are then assigned to source.p. This
133 can be a single time series (in which case it is applied to all
134 source elements), or a matrix of time series following the source
135 elements using MATLAB's standard column-wise linear matrix index
136 ordering. A time varying velocity source can be specified in an
137 analogous fashion, where the source location is specified by
138 source.u_mask, and the time varying input velocity is assigned to
139 source.ux and source.uy.
141 The field values are returned as arrays of time series at the sensor
142 locations defined by sensor.mask. This can be defined in three
143 different ways. (1) As a binary matrix (i.e., a matrix of 1's and 0's
144 with the same dimensions as the computational grid) representing the
145 grid points within the computational grid that will collect the data.
146 (2) As the grid coordinates of two opposing corners of a rectangle in
147 the form [x1; y1; x2; y2]. This is equivalent to using a binary
148 sensor mask covering the same region, however, the output is indexed
149 differently as discussed below. (3) As a series of Cartesian
150 coordinates within the grid which specify the location of the
151 pressure values stored at each time step. If the Cartesian
152 coordinates don't exactly match the coordinates of a grid point, the
153 output values are calculated via interpolation. The Cartesian points
154 must be given as a 2 by N matrix corresponding to the x and y
155 positions, respectively, where the Cartesian origin is assumed to be
156 in the center of the grid. If no output is required, the sensor input
157 can be replaced with an empty array [].
159 If sensor.mask is given as a set of Cartesian coordinates, the
160 computed sensor_data is returned in the same order. If sensor.mask is
161 given as a binary matrix, sensor_data is returned using MATLAB's
162 standard column-wise linear matrix index ordering. In both cases, the
163 recorded data is indexed as sensor_data(sensor_point_index,
164 time_index). For a binary sensor mask, the field values at a
165 particular time can be restored to the sensor positions within the
166 computation grid using unmaskSensorData. If sensor.mask is given as a
167 list of opposing corners of a rectangle, the recorded data is indexed
168 as sensor_data(rect_index).p(x_index, y_index, time_index), where
169 x_index and y_index correspond to the grid index within the
170 rectangle, and rect_index corresponds to the number of rectangles if
171 more than one is specified.
173 By default, the recorded acoustic pressure field is passed directly
174 to the output sensor_data. However, other acoustic parameters can
175 also be recorded by setting sensor.record to a cell array of the form
176 {'p', 'u', 'p_max', ...}. For example, both the particle velocity and
177 the acoustic pressure can be returned by setting sensor.record =
178 {'p', 'u'}. If sensor.record is given, the output sensor_data is
179 returned as a structure with the different outputs appended as
180 structure fields. For example, if sensor.record = {'p', 'p_final',
181 'p_max', 'u'}, the output would contain fields sensor_data.p,
182 sensor_data.p_final, sensor_data.p_max, sensor_data.ux, and
183 sensor_data.uy. Most of the output parameters are recorded at the
184 given sensor positions and are indexed as
185 sensor_data.field(sensor_point_index, time_index) or
186 sensor_data(rect_index).field(x_index, y_index, time_index) if using
187 a sensor mask defined as opposing rectangular corners. The exceptions
188 are the averaged quantities ('p_max', 'p_rms', 'u_max', 'p_rms',
189 'I_avg'), the 'all' quantities ('p_max_all', 'p_min_all',
190 'u_max_all', 'u_min_all'), and the final quantities ('p_final',
191 'u_final'). The averaged quantities are indexed as
192 sensor_data.p_max(sensor_point_index) or
193 sensor_data(rect_index).p_max(x_index, y_index) if using rectangular
194 corners, while the final and 'all' quantities are returned over the
195 entire grid and are always indexed as sensor_data.p_final(nx, ny),
196 regardless of the type of sensor mask.
198 kspaceFirstOrder2D may also be used for time reversal image
199 reconstruction by assigning the time varying pressure recorded over
200 an arbitrary sensor surface to the input field
201 sensor.time_reversal_boundary_data. This data is then enforced in
202 time reversed order as a time varying Dirichlet boundary condition
203 over the sensor surface given by sensor.mask. The boundary data must
204 be indexed as sensor.time_reversal_boundary_data(sensor_point_index,
205 time_index). If sensor.mask is given as a set of Cartesian
206 coordinates, the boundary data must be given in the same order. An
207 equivalent binary sensor mask (computed using nearest neighbour
208 interpolation) is then used to place the pressure values into the
209 computational grid at each time step. If sensor.mask is given as a
210 binary matrix of sensor points, the boundary data must be ordered
211 using MATLAB's standard column-wise linear matrix indexing. If no
212 additional inputs are required, the source input can be replaced with
213 an empty array [].
215 Acoustic attenuation compensation can also be included during time
216 reversal image reconstruction by assigning the absorption parameters
217 medium.alpha_coeff and medium.alpha_power and reversing the sign of
218 the absorption term by setting medium.alpha_sign = [-1, 1]. This
219 forces the propagating waves to grow according to the absorption
220 parameters instead of decay. The reconstruction should then be
221 regularised by assigning a filter to medium.alpha_filter (this can be
222 created using getAlphaFilter).
224 Note: To run a simple photoacoustic image reconstruction example
225 using time reversal (that commits the 'inverse crime' of using the
226 same numerical parameters and model for data simulation and image
227 reconstruction), the sensor_data returned from a k-Wave simulation
228 can be passed directly to sensor.time_reversal_boundary_data with the
229 input fields source.p0 and source.p removed or set to zero.
231 Args:
232 kgrid: kWaveGrid instance
233 medium: kWaveMedium instance
234 source: kWaveSource instance
235 sensor: kWaveSensor instance
236 **kwargs:
238 Returns:
240 """
241 # start the timer and store the start time
242 TicToc.tic()
244 k_sim = kWaveSimulation(kgrid, medium, source, sensor, **kwargs)
245 k_sim.input_checking('kspaceFirstOrder2D')
247 # =========================================================================
248 # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID
249 # =========================================================================
250 options = k_sim.options
252 # interpolate the values of the density at the staggered grid locations
253 # where sgx = (x + dx/2, y, z), sgy = (x, y + dy/2, z), sgz = (x, y, z + dz/2)
254 k_sim.rho0 = np.atleast_1d(k_sim.rho0)
255 if k_sim.rho0.ndim == 2 and options.use_sg:
256 # rho0 is heterogeneous and staggered grids are used
257 grid_points = [k_sim.kgrid.x, k_sim.kgrid.y]
258 k_sim.rho0_sgx = interpolate2D(grid_points, k_sim.rho0, [k_sim.kgrid.x + k_sim.kgrid.dx / 2, k_sim.kgrid.y])
259 k_sim.rho0_sgy = interpolate2D(grid_points, k_sim.rho0, [k_sim.kgrid.x, k_sim.kgrid.y + k_sim.kgrid.dy / 2])
260 else:
261 # rho0 is homogeneous or staggered grids are not used
262 k_sim.rho0_sgx = k_sim.rho0
263 k_sim.rho0_sgy = k_sim.rho0
264 k_sim.rho0_sgz = None
266 # invert rho0 so it doesn't have to be done each time step
267 k_sim.rho0_sgx_inv = 1 / k_sim.rho0_sgx
268 k_sim.rho0_sgy_inv = 1 / k_sim.rho0_sgy
270 # clear unused variables if not using them in _saveToDisk
271 if not options.save_to_disk:
272 del k_sim.rho0_sgx
273 del k_sim.rho0_sgy
275 # =========================================================================
276 # PREPARE DERIVATIVE AND PML OPERATORS
277 # =========================================================================
279 # get the PML operators based on the reference sound speed and PML settings
280 Nx, Ny = k_sim.kgrid.Nx, k_sim.kgrid.Ny
281 dx, dy = k_sim.kgrid.dx, k_sim.kgrid.dy
282 dt = k_sim.kgrid.dt
283 pml_x_alpha, pml_y_alpha = options.pml_x_alpha, options.pml_y_alpha
284 pml_x_size, pml_y_size = options.pml_x_size, options.pml_y_size
285 c_ref = k_sim.c_ref
287 k_sim.pml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, False, 1)
288 k_sim.pml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, True and options.use_sg, 1)
289 k_sim.pml_y = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, False, 2)
290 k_sim.pml_y_sgy = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, True and options.use_sg, 2)
292 # define the k-space derivative operators, multiply by the staggered
293 # grid shift operators, and then re-order using ifftshift (the option
294 # flgs.use_sg exists for debugging)
295 kx_vec, ky_vec = k_sim.kgrid.k_vec
296 kx_vec, ky_vec = np.array(kx_vec), np.array(ky_vec)
297 if options.use_sg:
298 k_sim.ddx_k_shift_pos = np.fft.ifftshift( 1j * kx_vec * np.exp( 1j * kx_vec * dx/2) )[None, :]
299 k_sim.ddx_k_shift_neg = np.fft.ifftshift( 1j * kx_vec * np.exp( -1j * kx_vec * dx/2) )[None, :]
300 k_sim.ddy_k_shift_pos = np.fft.ifftshift( 1j * ky_vec * np.exp( 1j * ky_vec * dy/2) )[None, :]
301 k_sim.ddy_k_shift_neg = np.fft.ifftshift( 1j * ky_vec * np.exp( -1j * ky_vec * dy/2) )[None, :]
302 else:
303 k_sim.ddx_k_shift_pos = np.fft.ifftshift( 1j * kx_vec )[None, :]
304 k_sim.ddx_k_shift_neg = np.fft.ifftshift( 1j * kx_vec )[None, :]
305 k_sim.ddy_k_shift_pos = np.fft.ifftshift( 1j * ky_vec )[None, :]
306 k_sim.ddy_k_shift_neg = np.fft.ifftshift( 1j * ky_vec )[None, :]
308 # force the derivative and shift operators to be in the correct direction for use with BSXFUN
309 k_sim.ddy_k_shift_pos = k_sim.ddy_k_shift_pos.T
310 k_sim.ddy_k_shift_neg = k_sim.ddy_k_shift_neg.T
312 # create k-space operators (the option flgs.use_kspace exists for debugging)
313 if options.use_kspace:
314 k = k_sim.kgrid.k
315 k_sim.kappa = np.fft.ifftshift(np.sinc(c_ref * k * dt / 2))
316 if (k_sim.source_p and k_sim.source.p_mode == 'additive') or ((k_sim.source_ux or k_sim.source_uy or k_sim.source_uz) and k_sim.source.u_mode == 'additive'):
317 k_sim.source_kappa = np.fft.ifftshift(np.cos(c_ref * k * dt / 2))
318 else:
319 k_sim.kappa = 1
320 k_sim.source_kappa = 1
322 # =========================================================================
323 # SAVE DATA TO DISK FOR RUNNING SIMULATION EXTERNAL TO MATLAB
324 # =========================================================================
326 # save to disk option for saving the input matrices to disk for running
327 # simulations using k-Wave++
328 if options.save_to_disk:
329 # store the pml size for resizing transducer object below
330 retract_size = [[options.pml_x_size, options.pml_y_size, options.pml_z_size]]
332 # run subscript to save files to disk
333 save_to_disk_func(k_sim.kgrid, k_sim.medium, k_sim.source, k_sim.options,
334 dotdict({
335 'ddx_k_shift_pos': k_sim.ddx_k_shift_pos,
336 'ddx_k_shift_neg': k_sim.ddx_k_shift_neg,
337 'dt': k_sim.dt,
338 'c0': k_sim.c0,
339 'c_ref': k_sim.c_ref,
340 'rho0': k_sim.rho0,
341 'rho0_sgx': k_sim.rho0_sgx,
342 'rho0_sgy': k_sim.rho0_sgy,
343 'rho0_sgz': k_sim.rho0_sgz,
344 'p_source_pos_index': k_sim.p_source_pos_index,
345 'u_source_pos_index': k_sim.u_source_pos_index,
346 's_source_pos_index': k_sim.s_source_pos_index,
347 'transducer_input_signal': k_sim.transducer_input_signal,
348 'delay_mask': k_sim.delay_mask,
349 'sensor_mask_index': k_sim.sensor_mask_index,
350 'record': k_sim.record,
351 }),
352 dotdict({
353 'source_p': k_sim.source_p,
354 'source_p0': k_sim.source_p0,
356 'source_ux': k_sim.source_ux,
357 'source_uy': k_sim.source_uy,
358 'source_uz': k_sim.source_uz,
360 'source_sxx': k_sim.source_sxx,
361 'source_syy': k_sim.source_syy,
362 'source_szz': k_sim.source_szz,
363 'source_sxy': k_sim.source_sxy,
364 'source_sxz': k_sim.source_sxz,
365 'source_syz': k_sim.source_syz,
367 'transducer_source': k_sim.transducer_source,
368 'nonuniform_grid': k_sim.nonuniform_grid,
369 'elastic_code': k_sim.elastic_code,
370 'axisymmetric': k_sim.axisymmetric,
371 'cuboid_corners': k_sim.cuboid_corners,
372 }))
374 # run subscript to resize the transducer object if the grid has been expanded
375 retract_transducer_grid_size(k_sim.source, k_sim.sensor, retract_size, k_sim.options.pml_inside)
377 # exit matlab computation if required
378 if options.save_to_disk_exit:
379 return
381 input_filename = k_sim.options.save_to_disk
382 output_filename = os.path.join(tempfile.gettempdir(), 'output.h5')
384 executor = Executor(device='gpu')
385 sensor_data = executor.run_simulation(input_filename, output_filename, options='--p_raw')
386 return k_sim.sensor.combine_sensor_data(sensor_data)