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