Coverage for kwave/kspaceFirstOrder2D.py: 20%

93 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-03-24 12:06 -0700

1from typing import Union 

2 

3import numpy as np 

4from beartype import beartype as typechecker 

5 

6from kwave.executor import Executor 

7from kwave.kgrid import kWaveGrid 

8from kwave.kmedium import kWaveMedium 

9from kwave.ksensor import kSensor 

10from kwave.ksource import kSource 

11from kwave.ktransducer import NotATransducer 

12from kwave.kWaveSimulation import kWaveSimulation 

13from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func 

14from kwave.options.simulation_execution_options import SimulationExecutionOptions 

15from kwave.options.simulation_options import SimulationOptions 

16from kwave.utils.dotdictionary import dotdict 

17from kwave.utils.interp import interpolate2d 

18from kwave.utils.pml import get_pml 

19from kwave.utils.tictoc import TicToc 

20 

21 

22@typechecker 

23def kspace_first_order_2d_gpu( 

24 kgrid: kWaveGrid, 

25 source: kSource, 

26 sensor: Union[NotATransducer, kSensor, None], 

27 medium: kWaveMedium, 

28 simulation_options: SimulationOptions, 

29 execution_options: SimulationExecutionOptions, 

30) -> np.ndarray: 

31 """ 

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

33 

34 kspaceFirstOrder2DG provides a blind interface to the C++/CUDA 

35 version of kspaceFirstOrder2D (called kspaceFirstOrder-CUDA) in the 

36 same way as kspaceFirstOrder3DC. Note, the C++ code does not support 

37 all input options, and all display options are ignored (only command 

38 line outputs are given). See the k-Wave user manual for more 

39 information. 

40 The function works by appending the optional input 'save_to_disk' to 

41 the user inputs and then calling kspaceFirstOrder2D to save the input 

42 files to disk. The contents of sensor.record (if set) are parsed as 

43 input flags, and the C++ code is run using the system command. The 

44 output files are then automatically loaded from disk and returned in 

45 the same fashion as kspaceFirstOrder2D. The input and output files 

46 are saved to the temporary directory native to the operating system, 

47 and are deleted after the function runs. 

48 This function requires the C++ binary/executable of 

49 kspaceFirstOrder-CUDA to be downloaded from 

50 http://www.k-wave.org/download.php and placed in the "binaries" 

51 directory of the k-Wave toolbox (the 2D and 3D code use the same 

52 binary). Alternatively, the name and location of the binary can be 

53 specified using the optional input parameters 'BinaryName' and 

54 'BinariesPath'. 

55 

56 This function is essentially a wrapper and directly uses the capabilities 

57 of kspaceFirstOrder3DC by replacing the binary name with the name of the 

58 GPU binary. 

59 """ 

60 execution_options.is_gpu_simulation = True # force to GPU 

61 assert isinstance(kgrid, kWaveGrid), "kgrid must be a kWaveGrid object" 

62 assert isinstance(medium, kWaveMedium), "medium must be a kWaveMedium object" 

63 assert isinstance(simulation_options, SimulationOptions), "simulation_options must be a SimulationOptions object" 

64 assert isinstance(execution_options, SimulationExecutionOptions), "execution_options must be a SimulationExecutionOptions object" 

65 

66 sensor_data = kspaceFirstOrder2D( 

67 kgrid=kgrid, source=source, sensor=sensor, medium=medium, simulation_options=simulation_options, execution_options=execution_options 

68 ) # pass inputs to CPU version 

69 return sensor_data 

70 

71 

72def kspaceFirstOrder2DC( 

73 kgrid: kWaveGrid, 

74 source: kSource, 

75 sensor: Union[NotATransducer, kSensor, None], 

76 medium: kWaveMedium, 

77 simulation_options: SimulationOptions, 

78 execution_options: SimulationExecutionOptions, 

79): 

80 """ 

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

82 

83 kspaceFirstOrder2DC provides a blind interface to the C++ version of 

84 kspaceFirstOrder2D (called kspaceFirstOrder-OMP) in the same way as 

85 kspaceFirstOrder3DC. Note, the C++ code does not support all input 

86 options, and all display options are ignored (only command line 

87 outputs are given). See the k-Wave user manual for more information. 

88 The function works by appending the optional input 'save_to_disk' to 

89 the user inputs and then calling kspaceFirstOrder2D to save the input 

90 files to disk. The contents of sensor.record (if set) are parsed as 

91 input flags, and the C++ code is run using the system command. The 

92 output files are then automatically loaded from disk and returned in 

93 the same fashion as kspaceFirstOrder2D. The input and output files 

94 are saved to the temporary directory native to the operating system, 

95 and are deleted after the function runs. 

96 For small simulations, running the simulation on a smaller number of 

97 cores can improve performance as the matrices are often small enough 

98 to fit within cache. It is recommended to adjust the value of 

99 'NumThreads' to optimise performance for a given simulation size and 

100 computer hardware. By default, simulations smaller than 128^2 are 

101 set to run using a single thread (this behaviour can be over-ridden 

102 using the 'NumThreads' option). In some circumstances, for very small 

103 simulations, the C++ code can be slower than the MATLAB code. 

104 This function requires the C++ binary/executable of 

105 kspaceFirstOrder-OMP to be downloaded from 

106 http://www.k-wave.org/download.php and placed in the "binaries" 

107 directory of the k-Wave toolbox (the same binary is used for 

108 simulations in 2D, 3D, and axisymmetric coordinates). Alternatively, 

109 the name and location of the binary can be specified using the 

110 optional input parameters 'BinaryName' and 'BinariesPath'. 

111 

112 This function is essentially a wrapper and directly uses the capabilities 

113 of kspaceFirstOrder3DC by replacing the binary name with the name of the 

114 GPU binary. 

115 

116 Args: 

117 kgrid: kWaveGrid instance 

118 source: kWaveSource instance 

119 sensor: NotATransducer or kSensor instance or None 

120 medium: kWaveMedium instance 

121 simulation_options: SimulationOptions instance 

122 execution_options: SimulationExecutionOptions instance 

123 

124 Returns: 

125 Sensor data as a numpy array 

126 """ 

127 execution_options.is_gpu_simulation = False # force to CPU 

128 # generate the input file and save to disk 

129 sensor_data = kspaceFirstOrder2D( 

130 kgrid=kgrid, source=source, sensor=sensor, medium=medium, simulation_options=simulation_options, execution_options=execution_options 

131 ) 

132 return sensor_data 

133 

134 

135@typechecker 

136def kspaceFirstOrder2D( 

137 kgrid: kWaveGrid, 

138 source: kSource, 

139 sensor: Union[NotATransducer, kSensor, None], 

140 medium: kWaveMedium, 

141 simulation_options: SimulationOptions, 

142 execution_options: SimulationExecutionOptions, 

143): 

144 """ 

145 2D time-domain simulation of wave propagation. 

146 

147 kspaceFirstOrder2D simulates the time-domain propagation of 

148 compressional waves through a two-dimensional homogeneous or 

149 heterogeneous acoustic medium given four input structures: kgrid, 

150 medium, source, and sensor. The computation is based on a first-order 

151 k-space model which accounts for power law absorption and a 

152 heterogeneous sound speed and density. If medium.BonA is specified, 

153 cumulative nonlinear effects are also modelled. At each time-step 

154 (defined by kgrid.dt and kgrid.Nt or kgrid.t_array), the acoustic 

155 field parameters at the positions defined by sensor.mask are recorded 

156 and stored. If kgrid.t_array is set to 'auto', this array is 

157 automatically generated using the makeTime method of the kWaveGrid 

158 class. An anisotropic absorbing boundary layer called a perfectly 

159 matched layer (PML) is implemented to prevent waves that leave one 

160 side of the domain being reintroduced from the opposite side (a 

161 consequence of using the FFT to compute the spatial derivatives in 

162 the wave equation). This allows infinite domain simulations to be 

163 computed using small computational grids. 

164 

165 For a homogeneous medium the formulation is exact and the time-steps 

166 are only limited by the effectiveness of the perfectly matched layer. 

167 For a heterogeneous medium, the solution represents a leap-frog 

168 pseudospectral method with a k-space correction that improves the 

169 accuracy of computing the temporal derivatives. This allows larger 

170 time-steps to be taken for the same level of accuracy compared to 

171 conventional pseudospectral time-domain methods. The computational 

172 grids are staggered both spatially and temporally. 

173 

174 An initial pressure distribution can be specified by assigning a 

175 matrix (the same size as the computational grid) of arbitrary numeric 

176 values to source.p0. A time varying pressure source can similarly be 

177 specified by assigning a binary matrix (i.e., a matrix of 1's and 0's 

178 with the same dimensions as the computational grid) to source.p_mask 

179 where the 1's represent the grid points that form part of the source. 

180 The time varying input signals are then assigned to source.p. This 

181 can be a single time series (in which case it is applied to all 

182 source elements), or a matrix of time series following the source 

183 elements using MATLAB's standard column-wise linear matrix index 

184 ordering. A time varying velocity source can be specified in an 

185 analogous fashion, where the source location is specified by 

186 source.u_mask, and the time varying input velocity is assigned to 

187 source.ux and source.uy. 

188 

189 The field values are returned as arrays of time series at the sensor 

190 locations defined by sensor.mask. This can be defined in three 

191 different ways. (1) As a binary matrix (i.e., a matrix of 1's and 0's 

192 with the same dimensions as the computational grid) representing the 

193 grid points within the computational grid that will collect the data. 

194 (2) As the grid coordinates of two opposing corners of a rectangle in 

195 the form [x1; y1; x2; y2]. This is equivalent to using a binary 

196 sensor mask covering the same region, however, the output is indexed 

197 differently as discussed below. (3) As a series of Cartesian 

198 coordinates within the grid which specify the location of the 

199 pressure values stored at each time step. If the Cartesian 

200 coordinates don't exactly match the coordinates of a grid point, the 

201 output values are calculated via interpolation. The Cartesian points 

202 must be given as a 2 by N matrix corresponding to the x and y 

203 positions, respectively, where the Cartesian origin is assumed to be 

204 in the center of the grid. If no output is required, the sensor input 

205 can be replaced with an empty array []. 

206 

207 If sensor.mask is given as a set of Cartesian coordinates, the 

208 computed sensor_data is returned in the same order. If sensor.mask is 

209 given as a binary matrix, sensor_data is returned using MATLAB's 

210 standard column-wise linear matrix index ordering. In both cases, the 

211 recorded data is indexed as sensor_data(sensor_point_index, 

212 time_index). For a binary sensor mask, the field values at a 

213 particular time can be restored to the sensor positions within the 

214 computation grid using unmaskSensorData. If sensor.mask is given as a 

215 list of opposing corners of a rectangle, the recorded data is indexed 

216 as sensor_data(rect_index).p(x_index, y_index, time_index), where 

217 x_index and y_index correspond to the grid index within the 

218 rectangle, and rect_index corresponds to the number of rectangles if 

219 more than one is specified. 

220 

221 By default, the recorded acoustic pressure field is passed directly 

222 to the output sensor_data. However, other acoustic parameters can 

223 also be recorded by setting sensor.record to a cell array of the form 

224 {'p', 'u', 'p_max', ...}. For example, both the particle velocity and 

225 the acoustic pressure can be returned by setting sensor.record = 

226 {'p', 'u'}. If sensor.record is given, the output sensor_data is 

227 returned as a structure with the different outputs appended as 

228 structure fields. For example, if sensor.record = {'p', 'p_final', 

229 'p_max', 'u'}, the output would contain fields sensor_data.p, 

230 sensor_data.p_final, sensor_data.p_max, sensor_data.ux, and 

231 sensor_data.uy. Most of the output parameters are recorded at the 

232 given sensor positions and are indexed as 

233 sensor_data.field(sensor_point_index, time_index) or 

234 sensor_data(rect_index).field(x_index, y_index, time_index) if using 

235 a sensor mask defined as opposing rectangular corners. The exceptions 

236 are the averaged quantities ('p_max', 'p_rms', 'u_max', 'p_rms', 

237 'I_avg'), the 'all' quantities ('p_max_all', 'p_min_all', 

238 'u_max_all', 'u_min_all'), and the final quantities ('p_final', 

239 'u_final'). The averaged quantities are indexed as 

240 sensor_data.p_max(sensor_point_index) or 

241 sensor_data(rect_index).p_max(x_index, y_index) if using rectangular 

242 corners, while the final and 'all' quantities are returned over the 

243 entire grid and are always indexed as sensor_data.p_final(nx, ny), 

244 regardless of the type of sensor mask. 

245 

246 kspaceFirstOrder2D may also be used for time reversal image 

247 reconstruction by assigning the time varying pressure recorded over 

248 an arbitrary sensor surface to the input field 

249 sensor.time_reversal_boundary_data. This data is then enforced in 

250 time reversed order as a time varying Dirichlet boundary condition 

251 over the sensor surface given by sensor.mask. The boundary data must 

252 be indexed as sensor.time_reversal_boundary_data(sensor_point_index, 

253 time_index). If sensor.mask is given as a set of Cartesian 

254 coordinates, the boundary data must be given in the same order. An 

255 equivalent binary sensor mask (computed using nearest neighbour 

256 interpolation) is then used to place the pressure values into the 

257 computational grid at each time step. If sensor.mask is given as a 

258 binary matrix of sensor points, the boundary data must be ordered 

259 using MATLAB's standard column-wise linear matrix indexing. If no 

260 additional inputs are required, the source input can be replaced with 

261 an empty array []. 

262 

263 Acoustic attenuation compensation can also be included during time 

264 reversal image reconstruction by assigning the absorption parameters 

265 medium.alpha_coeff and medium.alpha_power and reversing the sign of 

266 the absorption term by setting medium.alpha_sign = [-1, 1]. This 

267 forces the propagating waves to grow according to the absorption 

268 parameters instead of decay. The reconstruction should then be 

269 regularised by assigning a filter to medium.alpha_filter (this can be 

270 created using getAlphaFilter). 

271 

272 Note: To run a simple photoacoustic image reconstruction example 

273 using time reversal (that commits the 'inverse crime' of using the 

274 same numerical parameters and model for data simulation and image 

275 reconstruction), the sensor_data returned from a k-Wave simulation 

276 can be passed directly to sensor.time_reversal_boundary_data with the 

277 input fields source.p0 and source.p removed or set to zero. 

278 

279 Args: 

280 kgrid: kWaveGrid instance 

281 medium: kWaveMedium instance 

282 source: kWaveSource instance 

283 sensor: kWaveSensor instance or None 

284 

285 Returns: 

286 

287 """ 

288 # start the timer and store the start time 

289 TicToc.tic() 

290 

291 # Currently we only support binary execution, meaning all simulations must be saved to disk. 

292 if not simulation_options.save_to_disk: 

293 if execution_options.is_gpu_simulation: 

294 raise ValueError("GPU simulation requires saving to disk. Please set SimulationOptions.save_to_disk=True") 

295 else: 

296 raise ValueError("CPU simulation requires saving to disk. Please set SimulationOptions.save_to_disk=True") 

297 

298 k_sim = kWaveSimulation(kgrid=kgrid, source=source, sensor=sensor, medium=medium, simulation_options=simulation_options) 

299 

300 k_sim.input_checking("kspaceFirstOrder2D") 

301 

302 # ========================================================================= 

303 # CALCULATE MEDIUM PROPERTIES ON STAGGERED GRID 

304 # ========================================================================= 

305 options = k_sim.options 

306 

307 # interpolate the values of the density at the staggered grid locations 

308 # where sgx = (x + dx/2, y, z), sgy = (x, y + dy/2, z), sgz = (x, y, z + dz/2) 

309 k_sim.rho0 = np.atleast_1d(k_sim.rho0) 

310 if k_sim.rho0.ndim == 2 and options.use_sg: 

311 # rho0 is heterogeneous and staggered grids are used 

312 grid_points = [k_sim.kgrid.x, k_sim.kgrid.y] 

313 k_sim.rho0_sgx = interpolate2d(grid_points, k_sim.rho0, [k_sim.kgrid.x + k_sim.kgrid.dx / 2, k_sim.kgrid.y]) 

314 k_sim.rho0_sgy = interpolate2d(grid_points, k_sim.rho0, [k_sim.kgrid.x, k_sim.kgrid.y + k_sim.kgrid.dy / 2]) 

315 else: 

316 # rho0 is homogeneous or staggered grids are not used 

317 k_sim.rho0_sgx = k_sim.rho0 

318 k_sim.rho0_sgy = k_sim.rho0 

319 k_sim.rho0_sgz = None 

320 

321 # invert rho0 so it doesn't have to be done each time step 

322 k_sim.rho0_sgx_inv = 1 / k_sim.rho0_sgx 

323 k_sim.rho0_sgy_inv = 1 / k_sim.rho0_sgy 

324 

325 # clear unused variables if not using them in _saveToDisk 

326 if not options.save_to_disk: 

327 del k_sim.rho0_sgx 

328 del k_sim.rho0_sgy 

329 

330 # ========================================================================= 

331 # PREPARE DERIVATIVE AND PML OPERATORS 

332 # ========================================================================= 

333 

334 # get the PML operators based on the reference sound speed and PML settings 

335 Nx, Ny = k_sim.kgrid.Nx, k_sim.kgrid.Ny 

336 dx, dy = k_sim.kgrid.dx, k_sim.kgrid.dy 

337 dt = k_sim.kgrid.dt 

338 pml_x_alpha, pml_y_alpha = options.pml_x_alpha, options.pml_y_alpha 

339 pml_x_size, pml_y_size = options.pml_x_size, options.pml_y_size 

340 c_ref = k_sim.c_ref 

341 

342 k_sim.pml_x = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, False, 1) 

343 k_sim.pml_x_sgx = get_pml(Nx, dx, dt, c_ref, pml_x_size, pml_x_alpha, True and options.use_sg, 1) 

344 k_sim.pml_y = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, False, 2) 

345 k_sim.pml_y_sgy = get_pml(Ny, dy, dt, c_ref, pml_y_size, pml_y_alpha, True and options.use_sg, 2) 

346 

347 # define the k-space derivative operators, multiply by the staggered 

348 # grid shift operators, and then re-order using ifftshift (the option 

349 # flgs.use_sg exists for debugging) 

350 kx_vec, ky_vec = k_sim.kgrid.k_vec 

351 kx_vec, ky_vec = np.array(kx_vec), np.array(ky_vec) 

352 if options.use_sg: 

353 k_sim.ddx_k_shift_pos = np.fft.ifftshift(1j * kx_vec * np.exp(1j * kx_vec * dx / 2))[None, :] 

354 k_sim.ddx_k_shift_neg = np.fft.ifftshift(1j * kx_vec * np.exp(-1j * kx_vec * dx / 2))[None, :] 

355 k_sim.ddy_k_shift_pos = np.fft.ifftshift(1j * ky_vec * np.exp(1j * ky_vec * dy / 2))[None, :] 

356 k_sim.ddy_k_shift_neg = np.fft.ifftshift(1j * ky_vec * np.exp(-1j * ky_vec * dy / 2))[None, :] 

357 else: 

358 k_sim.ddx_k_shift_pos = np.fft.ifftshift(1j * kx_vec)[None, :] 

359 k_sim.ddx_k_shift_neg = np.fft.ifftshift(1j * kx_vec)[None, :] 

360 k_sim.ddy_k_shift_pos = np.fft.ifftshift(1j * ky_vec)[None, :] 

361 k_sim.ddy_k_shift_neg = np.fft.ifftshift(1j * ky_vec)[None, :] 

362 

363 # force the derivative and shift operators to be in the correct direction for use with BSXFUN 

364 k_sim.ddy_k_shift_pos = k_sim.ddy_k_shift_pos.T 

365 k_sim.ddy_k_shift_neg = k_sim.ddy_k_shift_neg.T 

366 

367 # create k-space operators (the option flgs.use_kspace exists for debugging) 

368 if options.use_kspace: 

369 k = k_sim.kgrid.k 

370 k_sim.kappa = np.fft.ifftshift(np.sinc(c_ref * k * dt / 2)) 

371 if (k_sim.source_p and k_sim.source.p_mode == "additive") or ( 

372 (k_sim.source_ux or k_sim.source_uy or k_sim.source_uz) and k_sim.source.u_mode == "additive" 

373 ): 

374 k_sim.source_kappa = np.fft.ifftshift(np.cos(c_ref * k * dt / 2)) 

375 else: 

376 k_sim.kappa = 1 

377 k_sim.source_kappa = 1 

378 

379 # ========================================================================= 

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

381 # ========================================================================= 

382 

383 # save to disk option for saving the input matrices to disk for running 

384 # simulations using k-Wave++ 

385 if options.save_to_disk: 

386 # store the pml size for resizing transducer object below 

387 retract_size = [[options.pml_x_size, options.pml_y_size, options.pml_z_size]] 

388 

389 # run subscript to save files to disk 

390 save_to_disk_func( 

391 k_sim.kgrid, 

392 k_sim.medium, 

393 k_sim.source, 

394 k_sim.options, 

395 execution_options.auto_chunking, 

396 dotdict( 

397 { 

398 "ddx_k_shift_pos": k_sim.ddx_k_shift_pos, 

399 "ddx_k_shift_neg": k_sim.ddx_k_shift_neg, 

400 "dt": k_sim.dt, 

401 "c0": k_sim.c0, 

402 "c_ref": k_sim.c_ref, 

403 "rho0": k_sim.rho0, 

404 "rho0_sgx": k_sim.rho0_sgx, 

405 "rho0_sgy": k_sim.rho0_sgy, 

406 "rho0_sgz": k_sim.rho0_sgz, 

407 "p_source_pos_index": k_sim.p_source_pos_index, 

408 "u_source_pos_index": k_sim.u_source_pos_index, 

409 "s_source_pos_index": k_sim.s_source_pos_index, 

410 "transducer_input_signal": k_sim.transducer_input_signal, 

411 "delay_mask": k_sim.delay_mask, 

412 "sensor_mask_index": k_sim.sensor_mask_index, 

413 "record": k_sim.record, 

414 } 

415 ), 

416 dotdict( 

417 { 

418 "source_p": k_sim.source_p, 

419 "source_p0": k_sim.source_p0, 

420 "source_ux": k_sim.source_ux, 

421 "source_uy": k_sim.source_uy, 

422 "source_uz": k_sim.source_uz, 

423 "source_sxx": k_sim.source_sxx, 

424 "source_syy": k_sim.source_syy, 

425 "source_szz": k_sim.source_szz, 

426 "source_sxy": k_sim.source_sxy, 

427 "source_sxz": k_sim.source_sxz, 

428 "source_syz": k_sim.source_syz, 

429 "transducer_source": k_sim.transducer_source, 

430 "nonuniform_grid": k_sim.nonuniform_grid, 

431 "elastic_code": k_sim.options.simulation_type.is_elastic_simulation(), 

432 "axisymmetric": k_sim.options.simulation_type.is_axisymmetric(), 

433 "cuboid_corners": k_sim.cuboid_corners, 

434 } 

435 ), 

436 ) 

437 

438 # run subscript to resize the transducer object if the grid has been expanded 

439 retract_transducer_grid_size(k_sim.source, k_sim.sensor, retract_size, k_sim.options.pml_inside) 

440 

441 # exit matlab computation if required 

442 if options.save_to_disk_exit: 

443 return 

444 

445 executor = Executor(simulation_options=simulation_options, execution_options=execution_options) 

446 executor_options = execution_options.as_list(sensor=k_sim.sensor) 

447 sensor_data = executor.run_simulation(k_sim.options.input_filename, k_sim.options.output_filename, options=executor_options) 

448 return sensor_data