Coverage for kwave/kspaceFirstOrder3D.py: 12%

1 import tempfile

2

3 from kwave.executor import Executor

4 from kwave.kWaveSimulation import kWaveSimulation

5 from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func

6 from kwave.kspaceFirstOrder import *

7 from kwave.utils import *

8

9

10@kspaceFirstOrderG

11 def 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()

53 def 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

96 def 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)