Coverage for kwave/kspaceFirstOrder2D.py: 16%

1 from numpy.fft import ifftshift

2 import tempfile

3

4 from kwave.executor import Executor

5 from kwave.kWaveSimulation import kWaveSimulation

6 from kwave.kWaveSimulation_helper import retract_transducer_grid_size, save_to_disk_func

7 from kwave.kspaceFirstOrder import *

8 from kwave.utils import *

9

10

11@kspaceFirstOrderG

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

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

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