Coverage for kwave/kspaceFirstOrder2D.py: 16%

74 statements  

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

1from numpy.fft import ifftshift 

2import tempfile 

3 

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 * 

9 

10 

11@kspaceFirstOrderG 

12def kspaceFirstOrder2DG(**kwargs): 

13 """ 

14 2D ime-domain simulation of wave propagation on a GPU using C++ CUDA code. 

15 

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

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 sensor_data = kspaceFirstOrder2DC(**kwargs) # pass inputs to CPU version 

43 return sensor_data 

44 

45 

46@kspaceFirstOrderC() 

47def kspaceFirstOrder2DC(**kwargs): 

48 """ 

49 2D time-domain simulation of wave propagation using C++ code. 

50 

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

79 

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. 

83 

84 Args: 

85 **kwargs: 

86 

87 Returns: 

88 

89 """ 

90 # generate the input file and save to disk 

91 kspaceFirstOrder2D(**kwargs) 

92 return kwargs['SaveToDisk'] 

93 

94 

95def kspaceFirstOrder2D(kgrid, medium, source, sensor, **kwargs): 

96 """ 

97 2D time-domain simulation of wave propagation. 

98 

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. 

116 

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. 

125 

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. 

140 

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

158 

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. 

172 

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. 

197 

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

214 

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

223 

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. 

230 

231 Args: 

232 kgrid: kWaveGrid instance 

233 medium: kWaveMedium instance 

234 source: kWaveSource instance 

235 sensor: kWaveSensor instance 

236 **kwargs: 

237 

238 Returns: 

239 

240 """ 

241 # start the timer and store the start time 

242 TicToc.tic() 

243 

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

245 k_sim.input_checking('kspaceFirstOrder2D') 

246 

247 # ========================================================================= 

248 # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID 

249 # ========================================================================= 

250 options = k_sim.options 

251 

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 

265 

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 

269 

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 

274 

275 # ========================================================================= 

276 # PREPARE DERIVATIVE AND PML OPERATORS 

277 # ========================================================================= 

278 

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 

286 

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) 

291 

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

307 

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 

311 

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 

321 

322 # ========================================================================= 

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

324 # ========================================================================= 

325 

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

331 

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, 

355 

356 'source_ux': k_sim.source_ux, 

357 'source_uy': k_sim.source_uy, 

358 'source_uz': k_sim.source_uz, 

359 

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, 

366 

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

373 

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) 

376 

377 # exit matlab computation if required 

378 if options.save_to_disk_exit: 

379 return 

380 

381 input_filename = k_sim.options.save_to_disk 

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

383 

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)