Coverage for kwave/kspaceFirstOrder3D.py: 12%

88 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-24 11:55 -0700

1import tempfile 

2 

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 * 

8 

9 

10@kspaceFirstOrderG 

11def kspaceFirstOrder3DG(**kwargs): 

12 """ 

13 3D time-domain simulation of wave propagation on a GPU using C++ CUDA code. 

14 

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. 

21 

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. 

30 

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'. 

37 

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 Args: 

43 **kwargs: 

44 

45 Returns: 

46 

47 """ 

48 sensor_data = kspaceFirstOrder3DC(**kwargs) # pass inputs to CPU version 

49 return sensor_data 

50 

51 

52@kspaceFirstOrderC() 

53def kspaceFirstOrder3DC(**kwargs): 

54 """ 

55 3D time-domain simulation of wave propagation using C++ code. 

56 

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. 

62 

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. 

71 

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. 

76 

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'. 

84 

85 Args: 

86 **kwargs: 

87 

88 Returns: 

89 

90 """ 

91 # generate the input file and save to disk 

92 sensor_data = kspaceFirstOrder3D(**kwargs) 

93 return sensor_data 

94 

95 

96def kspaceFirstOrder3D(kgrid, medium, source, sensor, **kwargs): 

97 """ 

98 3D time-domain simulation of wave propagation. 

99 

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. 

117 

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. 

126 

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. 

141 

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. 

161 

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. 

175 

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. 

201 

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 []. 

218 

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). 

227 

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. 

234 

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. 

240 

241 Args: 

242 kgrid: kWaveGrid instance 

243 medium: kWaveMedium instance 

244 source: kWaveSource instance 

245 sensor: kWaveSensor instance 

246 **kwargs: 

247 

248 Returns: 

249 

250 """ 

251 # start the timer and store the start time 

252 TicToc.tic() 

253 

254 k_sim = kWaveSimulation(kgrid, medium, source, sensor, **kwargs) 

255 k_sim.input_checking('kspaceFirstOrder3D') 

256 

257 # ========================================================================= 

258 # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID 

259 # ========================================================================= 

260 options = k_sim.options 

261 

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 

276 

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 

281 

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 

287 

288 # ========================================================================= 

289 # PREPARE DERIVATIVE AND PML OPERATORS 

290 # ========================================================================= 

291 

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 

299 

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) 

306 

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 

326 

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 

330 

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 

334 

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 

338 

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 

348 

349 # ========================================================================= 

350 # SAVE DATA TO DISK FOR RUNNING SIMULATION EXTERNAL TO MATLAB 

351 # ========================================================================= 

352 

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]] 

358 

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, 

382 

383 'source_ux': k_sim.source_ux, 

384 'source_uy': k_sim.source_uy, 

385 'source_uz': k_sim.source_uz, 

386 

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, 

393 

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 })) 

400 

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) 

403 

404 # exit matlab computation if required 

405 if options.save_to_disk_exit: 

406 return 

407 

408 input_filename = k_sim.options.save_to_disk 

409 output_filename = os.path.join(tempfile.gettempdir(), 'output.h5') 

410 

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)