Coverage for kwave/utils/kutils.py: 8%

421 statements  

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

1from copy import deepcopy 

2from math import floor 

3from typing import Union, List, Optional 

4from numpy import ndarray, array 

5from kwave.utils.matrixutils import unflatten_matlab_mask, matlab_mask 

6 

7from kwave.utils.checkutils import num_dim 

8from kwave.utils.conversionutils import scale_SI 

9 

10from kwave.kgrid import kWaveGrid 

11import numpy as np 

12import warnings 

13import scipy 

14from numpy.fft import ifftshift, fft, ifft, fftshift 

15 

16from .misc import sinc, ndgrid, gaussian 

17from .conversionutils import db2neper 

18 

19import math 

20 

21 

22def primefactors(n): 

23 # even number divisible 

24 factors = [] 

25 while n % 2 == 0: 

26 factors.append(2), 

27 n = n / 2 

28 

29 # n became odd 

30 for i in range(3, int(math.sqrt(n)) + 1, 2): 

31 

32 while (n % i == 0): 

33 factors.append(i) 

34 n = n / i 

35 

36 if n > 2: 

37 factors.append(n) 

38 

39 return factors 

40 

41 

42def check_factors(min_number, max_number): 

43 """ 

44 Return the maximum prime factor for a range of numbers. 

45 

46 checkFactors loops through the given range of numbers and finds the 

47 numbers with the smallest maximum prime factors. This allows suitable 

48 grid sizes to be selected to maximise the speed of the FFT (this is 

49 fastest for FFT lengths with small prime factors). The output is 

50 printed to the command line, and a plot of the factors is generated. 

51 

52 Args: 

53 min_number: integer specifying the lower bound of values to test 

54 max_number: integer specifying the upper bound of values to test 

55 

56 Returns: 

57 

58 """ 

59 

60 # extract factors 

61 facs = np.zeros(1, max_number - min_number) 

62 fac_max = facs 

63 for index in range(min_number, max_number): 

64 facs[index - min_number + 1] = len(primefactors(index)) 

65 fac_max[index - min_number + 1] = max(primefactors(index)) 

66 

67 # compute best factors in range 

68 print('Numbers with a maximum prime factor of 2') 

69 ind = min_number + np.argwhere(fac_max == 2) 

70 print(ind) 

71 print('Numbers with a maximum prime factor of 3') 

72 ind = min_number + np.argwhere(fac_max == 3) 

73 print(ind) 

74 print('Numbers with a maximum prime factor of 5') 

75 ind = min_number + np.argwhere(fac_max == 5) 

76 print(ind) 

77 print('Numbers with a maximum prime factor of 7') 

78 ind = min_number + np.argwhere(fac_max == 7) 

79 print(ind) 

80 print('Numbers to avoid (prime numbers)') 

81 nums = np.arange(min_number, max_number) 

82 print(nums[fac_max == nums]) 

83 

84 

85def check_stability(kgrid, medium): 

86 """ 

87 checkStability calculates the maximum time step for which the k-space 

88 propagation models kspaceFirstOrder1D, kspaceFirstOrder2D and 

89 kspaceFirstOrder3D are stable. These models are unconditionally 

90 stable when the reference sound speed is equal to or greater than the 

91 maximum sound speed in the medium and there is no absorption. 

92 However, when the reference sound speed is less than the maximum 

93 sound speed the model is only stable for sufficiently small time 

94 steps. The criterion is more stringent (the time step is smaller) in 

95 the absorbing case. 

96 

97 The time steps given are accurate when the medium properties are 

98 homogeneous. For a heterogeneous media they give a useful, but not 

99 exact, estimate. 

100 Args: 

101 kgrid: k-Wave grid object return by kWaveGrid 

102 medium: structure containing the medium properties 

103 

104 Returns: the maximum time step for which the models are stable. 

105 This is set to Inf when the model is unconditionally stable. 

106 """ 

107 # why? : this function was migrated from Matlab. 

108 # Matlab would treat the 'medium' as a "pass by value" argument. 

109 # In python argument is passed by reference and changes in this function will cause original data to be changed. 

110 # Instead of making significant changes to the function, we make a deep copy of the argument 

111 medium = deepcopy(medium) 

112 

113 # define literals 

114 FIXED_POINT_ACCURACY = 1e-12 

115 

116 # find the maximum wavenumber 

117 kmax = kgrid.k.max() 

118 

119 # calculate the reference sound speed for the fluid code, using the 

120 # maximum by default which ensures the model is unconditionally stable 

121 reductions = { 

122 'min': np.min, 

123 'max': np.max, 

124 'mean': np.mean 

125 } 

126 

127 if medium.sound_speed_ref is not None: 

128 ss_ref = medium.sound_speed_ref 

129 if np.isscalar(ss_ref): 

130 c_ref = ss_ref 

131 else: 

132 try: 

133 c_ref = reductions[ss_ref](medium.sound_speed) 

134 except KeyError: 

135 raise NotImplementedError('Unknown input for medium.sound_speed_ref.') 

136 else: 

137 c_ref = reductions['max'](medium.sound_speed) 

138 

139 # calculate the timesteps required for stability 

140 if medium.alpha_coeff is None or np.all(medium.alpha_coeff == 0): 

141 

142 # ===================================================================== 

143 # NON-ABSORBING CASE 

144 # ===================================================================== 

145 

146 medium.sound_speed = np.atleast_1d(medium.sound_speed) 

147 if c_ref >= medium.sound_speed.max(): 

148 # set the timestep to Inf when the model is unconditionally stable 

149 dt_stability_limit = float('inf') 

150 

151 else: 

152 # set the timestep required for stability when c_ref~=max(medium.sound_speed(:)) 

153 dt_stability_limit = 2 / (c_ref * kmax) * np.asin(c_ref / medium.sound_speed.max()) 

154 

155 else: 

156 

157 # ===================================================================== 

158 # ABSORBING CASE 

159 # ===================================================================== 

160 

161 # convert the absorption coefficient to nepers.(rad/s)^-y.m^-1 

162 medium.alpha_coeff = db2neper(medium.alpha_coeff, medium.alpha_power) 

163 

164 # calculate the absorption constant 

165 if medium.alpha_mode == 'no_absorption': 

166 absorb_tau = -2 * medium.alpha_coeff * medium.sound_speed ** (medium.alpha_power - 1) 

167 else: 

168 absorb_tau = np.array([0]) 

169 

170 # calculate the dispersion constant 

171 if medium.alpha_mode == 'no_dispersion': 

172 absorb_eta = 2 * medium.alpha_coeff * medium.sound_speed ** medium.alpha_power * np.tan( 

173 np.pi * medium.alpha_power / 2) 

174 else: 

175 absorb_eta = np.array([0]) 

176 

177 # estimate the timestep required for stability in the absorbing case by 

178 # assuming the k-space correction factor, kappa = 1 (note that 

179 # absorb_tau and absorb_eta are negative quantities) 

180 medium.sound_speed = np.atleast_1d(medium.sound_speed) 

181 

182 temp1 = medium.sound_speed.max() * absorb_tau.min() * kmax ** (medium.alpha_power - 1) 

183 temp2 = 1 - absorb_eta.min() * kmax ** (medium.alpha_power - 1) 

184 dt_estimate = (temp1 + np.sqrt(temp1 ** 2 + 4 * temp2)) / (temp2 * kmax * medium.sound_speed.max()) 

185 

186 # use a fixed point iteration to find the correct timestep, assuming 

187 # now that kappa = kappa(dt), using the previous estimate as a starting 

188 # point 

189 

190 # first define the function to iterate 

191 def kappa(dt): 

192 return sinc(c_ref * kmax * dt / 2) 

193 

194 def temp3(dt): 

195 return medium.sound_speed.max() * absorb_tau.min() * kappa(dt) * kmax ** (medium.alpha_power - 1) 

196 

197 def func_to_solve(dt): 

198 return (temp3(dt) + np.sqrt((temp3(dt)) ** 2 + 4 * temp2)) / ( 

199 temp2 * kmax * kappa(dt) * medium.sound_speed.max()) 

200 

201 # run the fixed point iteration 

202 dt_stability_limit = dt_estimate 

203 dt_old = 0 

204 while abs(dt_stability_limit - dt_old) > FIXED_POINT_ACCURACY: 

205 dt_old = dt_stability_limit 

206 dt_stability_limit = func_to_solve(dt_stability_limit) 

207 

208 return dt_stability_limit 

209 

210 

211def add_noise(signal, snr, mode="rms"): 

212 """ 

213 

214 Args: 

215 signal (np.array): input signal 

216 snr (float): desired signal snr (signal-to-noise ratio) in decibels after adding noise 

217 mode (str): 'rms' (default) or 'peak' 

218 

219 Returns: 

220 signal (np.array): signal with augmented with noise. This behaviour differs from the k-Wave MATLAB implementation in that the SNR is nor returned. 

221 

222 """ 

223 if mode == "rms": 

224 reference = np.sqrt(np.mean(signal ** 2)) 

225 elif mode == "peak": 

226 reference = np.max(signal) 

227 else: 

228 raise ValueError(f"Unknown parameter '{mode}' for input mode.") 

229 

230 # calculate the standard deviation of the Gaussian noise 

231 std_dev = reference / (10 ** (snr / 20)) 

232 

233 # calculate noise 

234 noise = std_dev * np.random.randn(*signal.shape) 

235 

236 # check the snr 

237 noise_rms = np.sqrt(np.mean(noise ** 2)) 

238 snr = 20. * np.log10(reference / noise_rms) 

239 

240 # add noise to the recorded sensor data 

241 signal = signal + noise 

242 

243 return signal 

244 

245 

246def grid2cart(input_kgrid: kWaveGrid, grid_selection: ndarray): 

247 """ 

248 Returns the Cartesian coordinates of the non-zero points of a binary grid. 

249 

250 DESCRIPTION: 

251 grid2cart returns the set of Cartesian coordinates corresponding to 

252 the non-zero elements in the binary matrix grid_data, in the 

253 coordinate framework defined in kgrid. 

254 

255 USAGE: 

256 [cart_data, order_index] = grid2cart(kgrid, grid_data) 

257 

258 args: 

259 input_kgrid: k-Wave grid object returned by kWaveGrid 

260 grid_selection: binary grid with the same dimensions as the k-Wave grid kgrid 

261 

262 Returns: 

263 cart_data: 1 x N, 2 x N, or 3 x N (for 1, 2, and 3 

264 dimensions) array of Cartesian sensor points 

265 order_index: returns a list of indices of the returned card_data coordinates. 

266 """ 

267 grid_data = np.array((grid_selection != 0), dtype=bool) 

268 cart_data = np.zeros((input_kgrid.dim, np.sum(grid_data))) 

269 

270 if input_kgrid.dim > 0: 

271 cart_data[0, :] = input_kgrid.x[grid_data] 

272 if input_kgrid.dim > 1: 

273 cart_data[1, :] = input_kgrid.y[grid_data] 

274 if input_kgrid.dim > 2: 

275 cart_data[2, :] = input_kgrid.z[grid_data] 

276 if 0 <= input_kgrid.dim > 3: 

277 raise ValueError("kGrid with unsupported size passed.") 

278 

279 order_index = np.argwhere(grid_data.squeeze() != 0) 

280 return cart_data.squeeze(), order_index 

281 

282 

283def get_win(N: Union[int, List[int]], 

284 # TODO: replace and refactor for scipy.signal.get_window 

285 # https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.get_window.html#scipy.signal.get_window 

286 type_: str, # TODO change this to enum in the future 

287 plot_win: bool = False, 

288 param: Optional[float] = None, 

289 rotation: bool = False, 

290 symmetric: bool = True, 

291 square: bool = False): 

292 """ 

293 Return a frequency domain windowing function 

294 getWin returns a 1D, 2D, or 3D frequency domain window of the 

295 specified type of the given dimensions. By default, higher 

296 dimensional windows are created using the outer product. The windows 

297 can alternatively be created using rotation by setting the optional 

298 input 'Rotation' to true. The coherent gain of the window can also be 

299 returned. 

300 Args: 

301 N: - number of samples, use. N = Nx for 1D | N = [Nx Ny] for 2D | N = [Nx Ny Nz] for 3D 

302 type_: - window type. Supported values are 

303 'Bartlett' 

304 'Bartlett-Hanning' 

305 'Blackman' 

306 'Blackman-Harris' 

307 'Blackman-Nuttall' 

308 'Cosine' 

309 'Flattop' 

310 'Gaussian' 

311 'HalfBand' 

312 'Hamming' 

313 'Hanning' 

314 'Kaiser' 

315 'Lanczos' 

316 'Nuttall' 

317 'Rectangular' 

318 'Triangular' 

319 'Tukey' 

320 plot_win: - Boolean controlling whether the window is displayed 

321 (default = false). 

322 

323 param: Control parameter for the Tukey, Blackman, Gaussian, 

324 and Kaiser windows: 

325 

326 Tukey: taper ratio (default = 0.5) 

327 Blackman: alpha (default = 0.16) 

328 Gaussian: standard deviation (default = 0.5) 

329 Kaiser: alpha (default = 3) 

330 

331 rotation: - Boolean controlling whether 2D and 3D windows are 

332 created via rotation or the outer product (default = 

333 false). Windows created via rotation will have edge 

334 values outside the window radius set to the first 

335 window value. 

336 

337 symmetric: - Boolean controlling whether the window is symmetrical 

338 (default = true). If set to false, a window of length N 

339 + 1 is created and the first N points are returned. For 

340 2D and 3D windows, 'Symmetric' can be defined as a 

341 vector defining the symmetry in each matrix dimension. 

342 

343 square: - Boolean controlling whether the window is forced to 

344 be square (default = false). If set to true and Nx 

345 and Nz are not equal, the window is created using the 

346 smaller variable, and then padded with zeros. 

347 

348 Returns: 

349 win: the window 

350 cg: the coherent gain of the window 

351 

352 """ 

353 

354 def cosineSeries(n, N, coeffs): 

355 """ 

356 Sub-function to calculate a summed filter cosine series. 

357 Args: 

358 n: 

359 N: 

360 coeffs: 

361 

362 Returns: 

363 

364 """ 

365 series = coeffs[0] 

366 for index in range(1, len(coeffs)): 

367 series = series + (-1) ** (index) * coeffs[index] * np.cos(index * 2 * np.pi * n / (N - 1)) 

368 return series.T 

369 

370 # Check if N is either `int` or `list of ints` 

371 # assert isinstance(N, int) or isinstance(N, list) or isinstance(N, np.ndarray) 

372 N = np.array(N, dtype=int) 

373 N = N if np.size(N) > 1 else int(N) 

374 

375 # Check if symmetric is either `bool` or `list of bools` 

376 # assert isinstance(symmetric, int) or isinstance(symmetric, list) 

377 symmetric = np.array(symmetric, dtype=bool) 

378 

379 # Set default value for `param` if type is one of the special ones 

380 assert not plot_win, NotImplementedError('Plotting is not implemented.') 

381 if type_ == 'Tukey': 

382 if param is None: 

383 param = 0.5 

384 param = np.clip(param, a_min=0, a_max=1) 

385 elif type_ == 'Blackman': 

386 if param is None: 

387 param = 0.16 

388 param = np.clip(param, a_min=0, a_max=1) 

389 elif type_ == 'Gaussian': 

390 if param is None: 

391 param = 0.5 

392 param = np.clip(param, a_min=0, a_max=0.5) 

393 elif type_ == 'Kaiser': 

394 if param is None: 

395 param = 3 

396 param = np.clip(param, a_min=0, a_max=100) 

397 

398 # if a non-symmetrical window is required, enlarge the window size (note, 

399 # this expands each dimension individually if symmetric is a vector) 

400 N = N + 1 * (1 - symmetric.astype(int)) 

401 

402 # if a square window is required, replace grid sizes with smallest size and 

403 # store a copy of the original size 

404 if square and (N.size != 1): 

405 N_orig = np.copy(N) 

406 L = min(N) 

407 N[:] = L 

408 

409 # create the window 

410 if N.size == 1: 

411 n = np.arange(0, N) 

412 

413 if type_ == 'Bartlett': 

414 win = (2 / (N - 1) * ((N - 1) / 2 - abs(n - (N - 1) / 2))).T 

415 elif type_ == 'Bartlett-Hanning': 

416 win = (0.62 - 0.48 * abs(n / (N - 1) - 1 / 2) - 0.38 * np.cos(2 * np.pi * n / (N - 1))).T 

417 elif type_ == 'Blackman': 

418 win = cosineSeries(n, N, [(1 - param) / 2, 0.5, param / 2]) 

419 elif type_ == 'Blackman-Harris': 

420 win = cosineSeries(n, N, [0.35875, 0.48829, 0.14128, 0.01168]) 

421 elif type_ == 'Blackman-Nuttall': 

422 win = cosineSeries(n, N, [0.3635819, 0.4891775, 0.1365995, 0.0106411]) 

423 elif type_ == 'Cosine': 

424 win = (np.cos(np.pi * n / (N - 1) - np.pi / 2)).T 

425 elif type_ == 'Flattop': 

426 win = cosineSeries(n, N, [0.21557895, 0.41663158, 0.277263158, 0.083578947, 0.006947368]) 

427 ylim = [-0.2, 1] 

428 elif type_ == 'Gaussian': 

429 win = (np.exp(-0.5 * ((n - (N - 1) / 2) / (param * (N - 1) / 2)) ** 2)).T 

430 elif type_ == 'HalfBand': 

431 win = np.ones(N) 

432 # why not to just round? => because rounding 0.5 introduces unexpected behaviour 

433 # round(0.5) should be 1 but it is 0 

434 ramp_length = round(N / 4 + 1e-8) 

435 ramp = 1 / 2 + 9 / 16 * np.cos(np.pi * np.arange(1, ramp_length + 1) / (2 * ramp_length)) - 1 / 16 * np.cos( 

436 3 * np.pi * np.arange(1, ramp_length + 1) / (2 * ramp_length)) 

437 if ramp_length > 0: 

438 win[0:ramp_length] = np.flip(ramp) 

439 win[-ramp_length:] = ramp 

440 elif type_ == 'Hamming': 

441 win = (0.54 - 0.46 * np.cos(2 * np.pi * n / (N - 1))).T 

442 elif type_ == 'Hanning': 

443 win = (0.5 - 0.5 * np.cos(2 * np.pi * n / (N - 1))).T 

444 elif type_ == 'Kaiser': 

445 part_1 = scipy.special.iv(0, np.pi * param * np.sqrt(1 - (2 * n / (N - 1) - 1) ** 2)) 

446 part_2 = scipy.special.iv(0, np.pi * param) 

447 win = part_1 / part_2 

448 elif type_ == 'Lanczos': 

449 win = 2 * np.pi * n / (N - 1) - np.pi 

450 win = sinc(win + 1e-12).T 

451 elif type_ == 'Nuttall': 

452 win = cosineSeries(n, N, [0.3635819, 0.4891775, 0.1365995, 0.0106411]) 

453 elif type_ == 'Rectangular': 

454 win = np.ones(N) 

455 elif type_ == 'Triangular': 

456 win = (2 / N * (N / 2 - abs(n - (N - 1) / 2))).T 

457 elif type_ == 'Tukey': 

458 win = np.ones((N, 1)) 

459 index = np.arange(0, (N - 1) * param / 2 + 1e-8) 

460 param = param * N 

461 win[0: len(index)] = 0.5 * (1 + np.cos(2 * np.pi / param * (index - param / 2)))[:, None] 

462 win[np.arange(-1, -len(index) - 1, -1)] = win[0:len(index)] 

463 win = win.squeeze(axis=-1) 

464 else: 

465 raise ValueError(f'Unknown window type: {type_}') 

466 

467 # trim the window if required 

468 if not symmetric: 

469 N -= 1 

470 win = win[0:N] 

471 win = np.expand_dims(win, axis=-1) 

472 

473 # calculate the coherent gain 

474 cg = win.sum() / N 

475 elif N.size == 2: 

476 input_options = { 

477 "param": param, 

478 "rotation": rotation, 

479 "symmetric": symmetric, 

480 "square": square 

481 } 

482 

483 # create the 2D window 

484 if rotation: 

485 

486 # create the window in one dimension using getWin recursively 

487 L = max(N) 

488 win_lin, _ = get_win(L, type_, param=param) 

489 win_lin = np.squeeze(win_lin) 

490 

491 # create the reference axis 

492 radius = (L - 1) / 2 

493 ll = np.linspace(-radius, radius, L) 

494 

495 # create the 2D window using rotation 

496 xx = np.linspace(-radius, radius, N[0]) 

497 yy = np.linspace(-radius, radius, N[1]) 

498 [x, y] = ndgrid(xx, yy) 

499 r = np.sqrt(x ** 2 + y ** 2) 

500 r[r > radius] = radius 

501 interp_func = scipy.interpolate.interp1d(ll, win_lin) 

502 win = interp_func(r) 

503 win[r <= radius] = interp_func(r[r <= radius]) 

504 

505 else: 

506 # create the window in each dimension using getWin recursively 

507 win_x, _ = get_win(N[0], type_, param=param) 

508 win_y, _ = get_win(N[1], type_, param=param) 

509 

510 # create the 2D window using the outer product 

511 win = (win_y * win_x.T).T 

512 

513 # trim the window if required 

514 N = N - 1 * (1 - np.array(symmetric).astype(int)) 

515 win = win[0:N[0], 0:N[1]] 

516 

517 # calculate the coherent gain 

518 cg = win.sum() / np.prod(N) 

519 elif N.size == 3: 

520 # create the 3D window 

521 if rotation: 

522 

523 # create the window in one dimension using getWin recursively 

524 L = N.max() 

525 win_lin, _ = get_win(L, type_, param=param) 

526 

527 # create the reference axis 

528 radius = (L - 1) / 2 

529 ll = np.linspace(-radius, radius, L) 

530 

531 # create the 3D window using rotation 

532 xx = np.linspace(-radius, radius, N[0]) 

533 yy = np.linspace(-radius, radius, N[1]) 

534 zz = np.linspace(-radius, radius, N[2]) 

535 [x, y, z] = ndgrid(xx, yy, zz) 

536 r = np.sqrt(x ** 2 + y ** 2 + z ** 2) 

537 r[r > radius] = radius 

538 

539 win_lin = np.squeeze(win_lin) 

540 interp_func = scipy.interpolate.interp1d(ll, win_lin) 

541 win = interp_func(r) 

542 win[r <= radius] = interp_func(r[r <= radius]) 

543 

544 else: 

545 

546 # create the window in each dimension using getWin recursively 

547 win_x, _ = get_win(N[0], type_, param=param) 

548 win_y, _ = get_win(N[1], type_, param=param) 

549 win_z, _ = get_win(N[2], type_, param=param) 

550 

551 # create the 2D window using the outer product 

552 win_2D = (win_x * win_z.T) 

553 

554 # create the 3D window 

555 win = np.zeros((N[0], N[1], N[2])) 

556 for index in range(0, N[1]): 

557 win[:, index, :] = win_2D[:, :] * win_y[index] 

558 

559 # trim the window if required 

560 N = N - 1 * (1 - np.array(symmetric).astype(int)) 

561 win = win[0:N[0], 0:N[1], 0:N[2]] 

562 

563 # calculate the coherent gain 

564 cg = win.sum() / np.prod(N) 

565 else: 

566 raise ValueError('Invalid input for N, only 1-, 2-, and 3-D windows are supported.') 

567 

568 # enlarge the window if required 

569 if square and (N.size != 1): 

570 L = N[0] 

571 win_sq = win 

572 win = np.zeros(N_orig) 

573 if N.size == 2: 

574 index1 = round((N[0] - L) / 2) 

575 index2 = round((N[1] - L) / 2) 

576 win[index1:(index1 + L), index2:(index2 + L)] = win_sq 

577 elif N.size == 3: 

578 index1 = floor((N_orig[0] - L) / 2) 

579 index2 = floor((N_orig[1] - L) / 2) 

580 index3 = floor((N_orig[2] - L) / 2) 

581 win[index1:index1 + L, index2:index2 + L, index3:index3 + L] = win_sq 

582 

583 return win, cg 

584 

585 

586def toneBurst(sample_freq, signal_freq, num_cycles, envelope='Gaussian', plot_signal=False, signal_length=0, 

587 signal_offset=0): 

588 """ 

589 Create an enveloped single frequency tone burst. 

590 toneBurst creates an enveloped single frequency tone burst for use in 

591 ultrasound simulations. If an array is given for the optional input 

592 'SignalOffset', a matrix of tone bursts is created where each row 

593 corresponds to a tone burst for each value of the 'SignalOffset'. If 

594 a value for the optional input 'SignalLength' is given, the tone 

595 burst/s are zero padded to this length (in samples). 

596 Args: 

597 plot_signal: 

598 signal_offset: 

599 signal_length: 

600 plot: 

601 sample_freq: sampling frequency [Hz] 

602 signal_freq: frequency of the tone burst signal [Hz] 

603 num_cycles: number of sinusoidal oscillations 

604 envelope: 

605 OPTIONAL INPUTS: 

606 Optional 'string', value pairs that may be used to modify the default 

607 computational settings. 

608 

609 'Envelope' - Envelope used to taper the tone burst. Valid inputs 

610 are: 

611 

612 'Gaussian' (the default) 

613 'Rectangular' 

614 [num_ring_up_cycles, num_ring_down_cycles] 

615 

616 The last option generates a continuous wave signal 

617 with a cosine taper of the specified length at the 

618 beginning and end. 

619 

620 'Plot' - Boolean controlling whether the created tone 

621 burst is plotted. 

622 'SignalLength' - Signal length in number of samples, if longer 

623 than the tone burst length, the signal is 

624 appended with zeros. 

625 'SignalOffset' - Signal offset before the tone burst starts in 

626 number of samples. 

627 

628 Returns: created tone burst 

629 

630 """ 

631 assert isinstance(signal_offset, int), "signal_offset must be integer" 

632 assert isinstance(signal_length, int), "signal_length must be integer" 

633 

634 # calculate the temporal spacing 

635 dt = 1 / sample_freq # [s] 

636 

637 # create the tone burst 

638 tone_length = num_cycles / signal_freq # [s] 

639 # We want to include the endpoint but only if it's divisible by the step-size 

640 if tone_length % dt < 1e-18: 

641 tone_t = np.linspace(0, tone_length, int(tone_length / dt) + 1) 

642 else: 

643 tone_t = np.arange(0, tone_length, dt) 

644 

645 tone_burst = np.sin(2 * np.pi * signal_freq * tone_t) 

646 tone_index = round(signal_offset) 

647 

648 # check for ring up and ring down input 

649 if isinstance(envelope, list) or isinstance(envelope, np.ndarray): # and envelope.size == 2: 

650 

651 # assign the inputs 

652 num_ring_up_cycles, num_ring_down_cycles = envelope 

653 

654 # check signal is long enough for ring up and down 

655 assert num_cycles >= (num_ring_up_cycles + num_ring_down_cycles), \ 

656 'Input num_cycles must be longer than num_ring_up_cycles + num_ring_down_cycles.' 

657 

658 # get period 

659 period = 1 / signal_freq 

660 

661 # create x-axis for ramp between 0 and pi 

662 up_ramp_length_points = round(num_ring_up_cycles * period / dt) 

663 down_ramp_length_points = round(num_ring_down_cycles * period / dt) 

664 up_ramp_axis = np.arange(0, np.pi + 1e-8, np.pi / (up_ramp_length_points - 1)) 

665 down_ramp_axis = np.arange(0, np.pi + 1e-8, np.pi / (down_ramp_length_points - 1)) 

666 

667 # create ramp using a shifted cosine 

668 up_ramp = (-np.cos(up_ramp_axis) + 1) * 0.5 

669 down_ramp = (np.cos(down_ramp_axis) + 1) * 0.5 

670 

671 # apply the ramps 

672 tone_burst[0:up_ramp_length_points] = tone_burst[0:up_ramp_length_points] * up_ramp 

673 tone_burst[-down_ramp_length_points:] = tone_burst[-down_ramp_length_points:] * down_ramp 

674 

675 else: 

676 

677 # create the envelope 

678 if envelope == 'Gaussian': 

679 x_lim = 3 

680 window_x = np.arange(-x_lim, x_lim + 1e-8, 2 * x_lim / (len(tone_burst) - 1)) 

681 window = gaussian(window_x, 1, 0, 1) 

682 elif envelope == 'Rectangular': 

683 window = np.ones_like(tone_burst) 

684 elif envelope == 'RingUpDown': 

685 raise NotImplementedError("RingUpDown not yet implemented") 

686 else: 

687 raise ValueError(f'Unknown envelope {envelope}.') 

688 

689 # apply the envelope 

690 tone_burst = tone_burst * window 

691 

692 # force the ends to be zero by applying a second window 

693 if envelope == 'Gaussian': 

694 tone_burst = tone_burst * np.squeeze(get_win(len(tone_burst), type_='Tukey', param=0.05)[0]) 

695 

696 # calculate the expected FWHM in the frequency domain 

697 # t_var = tone_length/(2*x_lim) 

698 # w_var = 1/(4*pi^2*t_var) 

699 # fw = 2 * sqrt(2 * log(2) * w_var) 

700 

701 # create the signal with the offset tone burst 

702 tone_index = np.array([tone_index]) 

703 signal_offset = np.array(signal_offset) 

704 if signal_length == 0: 

705 signal = np.zeros((tone_index.size, signal_offset.max() + len(tone_burst))) 

706 else: 

707 signal = np.zeros([tone_index.size, signal_length]) 

708 

709 for offset in range(tone_index.size): 

710 signal[offset, tone_index[offset]:tone_index[offset] + len(tone_burst)] = tone_burst.T 

711 

712 # plot the signal if required 

713 if plot_signal: 

714 raise NotImplementedError 

715 

716 return signal 

717 

718 

719def reorder_binary_sensor_data(sensor_data: np.ndarray, reorder_index: np.ndarray): 

720 """ 

721 

722 Args: 

723 sensor_data: N x K 

724 reorder_index: N 

725 

726 Returns: 

727 

728 """ 

729 reorder_index = np.squeeze(reorder_index) 

730 assert sensor_data.ndim == 2 

731 assert reorder_index.ndim == 1 

732 

733 return sensor_data[reorder_index.argsort()] 

734 

735 

736def calc_max_freq(max_spat_freq, c): 

737 filter_cutoff_freq = max_spat_freq * c / (2 * np.pi) 

738 return filter_cutoff_freq 

739 

740 

741def freq2wavenumber(N, k_max, filter_cutoff, c, k_dim): 

742 """ 

743 Args: 

744 N: 

745 k_max: 

746 filter_cutoff: 

747 c: 

748 k_dim: 

749 

750 Returns: 

751 

752 """ 

753 k_cutoff = 2 * np.pi * filter_cutoff / c 

754 

755 # set the alpha_filter size 

756 filter_size = round(N * k_cutoff / k_dim[-1]) 

757 

758 # check the alpha_filter size 

759 if filter_size > N: 

760 # set the alpha_filter size to be the same as the grid size 

761 filter_size = N 

762 filter_cutoff = k_max * c / (2 * np.pi) 

763 return filter_size, filter_cutoff 

764 

765 

766def get_alpha_filter(kgrid, medium, filter_cutoff, taper_ratio=0.5): 

767 """ 

768 getAlphaFilter uses get_win to create a Tukey window via rotation to 

769 pass to the medium.alpha_filter input field of the first order 

770 simulation functions (kspaceFirstOrder1D, kspaceFirstOrder2D, and 

771 kspaceFirstOrder3D). This parameter is used to regularise time 

772 reversal image reconstruction when absorption compensation is 

773 included. 

774 

775 Args: 

776 kgrid (kWaveGrid): 

777 medium (Medium): 

778 filter_cutoff (list): Any of the filter_cutoff inputs may be set to 'max' to set the cutoff frequency to the maximum frequency supported by the grid 

779 taper_ratio: 

780 

781 Returns: 

782 alpha_filter: 

783 """ 

784 

785 dim = num_dim(kgrid.k) 

786 print(f' taber ratio: {taper_ratio}') 

787 # extract the maximum sound speed 

788 c = max(medium.sound_speed) 

789 

790 assert len(filter_cutoff) == dim, f"Input filter_cutoff must have {dim} elements for a {dim}D grid" 

791 

792 # parse cutoff freqs 

793 filter_size = [] 

794 for idx, freq in enumerate(filter_cutoff): 

795 if freq == 'max': 

796 filter_cutoff[idx] = calc_max_freq(kgrid.k_max[idx], c) 

797 filter_size_local = kgrid.N[idx] 

798 else: 

799 filter_size_local, filter_cutoff[idx] = freq2wavenumber(kgrid.N[idx], kgrid.k_max[idx], filter_cutoff[idx], 

800 c, kgrid.k[idx]) 

801 filter_size.append(filter_size_local) 

802 

803 # create the alpha_filter 

804 filter_sec, _ = get_win(filter_size, 'Tukey', param=taper_ratio, rotation=True) 

805 

806 # enlarge the alpha_filter to the size of the grid 

807 alpha_filter = np.zeros(kgrid.N) 

808 indexes = [round((kgrid.N[idx] - filter_size[idx]) / 2) for idx in range(len(filter_size))] 

809 

810 if dim == 1: 

811 alpha_filter[indexes[0]: indexes[0] + filter_size[0]] 

812 elif dim == 2: 

813 alpha_filter[indexes[0]: indexes[0] + filter_size[0], indexes[1]: indexes[1] + filter_size[1]] = filter_sec 

814 elif dim == 3: 

815 alpha_filter[indexes[0]: indexes[0] + filter_size[0], indexes[1]: indexes[1] + filter_size[1], 

816 indexes[2]:indexes[2] + filter_size[2]] = filter_sec 

817 

818 dim_string = lambda cutoff_vals: "".join([str(scale_SI(co)[0]) + " Hz by " for co in cutoff_vals]) 

819 # update the command line status 

820 print(f' filter cutoff: ' + dim_string(filter_cutoff)[:-4] + '.') 

821 

822 return alpha_filter 

823 

824 

825def focus(kgrid, input_signal, source_mask, focus_position, sound_speed): 

826 """ 

827 focus Create input signal based on source mask and focus position. 

828 focus takes a single input signal and a source mask and creates an 

829 input signal matrix (with one input signal for each source point). 

830 The appropriate time delays required to focus the signals at a given 

831 position in Cartesian space are automatically added based on the user 

832 inputs for focus_position and sound_speed. 

833 

834 Args: 

835 kgrid: k-Wave grid object returned by kWaveGrid 

836 input_signal: single time series input 

837 source_mask: matrix specifying the positions of the time 

838 varying source distribution (i.e., source.p_mask 

839 or source.u_mask) 

840 focus_position: position of the focus in Cartesian coordinates 

841 sound_speed: scalar sound speed 

842 

843 Returns: 

844 input_signal_mat: matrix of time series following the source points 

845 """ 

846 

847 assert kgrid.t_array != 'auto', "kgrid.t_array must be defined." 

848 if isinstance(sound_speed, int): 

849 sound_speed = float(sound_speed) 

850 

851 assert isinstance(sound_speed, float), "sound_speed must be a scalar." 

852 

853 positions = [kgrid.x.flatten(), kgrid.y.flatten(), kgrid.z.flatten()] 

854 

855 # filter_positions 

856 positions = [position for position in positions if (position != np.nan).any()] 

857 assert len(positions) == kgrid.dim 

858 positions = np.array(positions) 

859 

860 if isinstance(focus_position, list): 

861 focus_position = np.array(focus_position) 

862 assert isinstance(focus_position, np.ndarray) 

863 

864 dist = np.linalg.norm(positions[:, source_mask.flatten() == 1] - focus_position[:, np.newaxis]) 

865 

866 # distance to delays 

867 delay = int(np.round(dist / (kgrid.dt * sound_speed))) 

868 max_delay = np.max(delay) 

869 rel_delay = -(delay - max_delay) 

870 

871 signal_mat = np.zeros((rel_delay.size, input_signal.size + max_delay)) 

872 

873 # for src_idx, delay in enumerate(rel_delay): 

874 # signal_mat[src_idx, delay:max_delay - delay] = input_signal 

875 # signal_mat[rel_delay, delay:max_delay - delay] = input_signal 

876 

877 warnings.warn("This method is not fully migrated, might be depricated and is untested.", PendingDeprecationWarning) 

878 return signal_mat 

879 

880 

881def broadcast_axis(data, ndims, axis): 

882 newshape = [1] * ndims 

883 newshape[axis] = -1 

884 return data.reshape(*newshape) 

885 

886 

887def get_wave_number(Nx, dx, dim): 

888 if Nx % 2 == 0: 

889 # even 

890 nx = np.arange(start=-Nx / 2, stop=Nx / 2) / Nx 

891 else: 

892 nx = np.arange(start=-(Nx - 1) / 2, stop=(Nx - 1) / 2 + 1) / Nx 

893 

894 kx = ifftshift((2 * np.pi / dx) * nx) 

895 

896 return kx 

897 

898 

899def gradient_spect(f, dn, dim=None, deriv_order=1): 

900 """ 

901 gradient_spect calculates the gradient of an n-dimensional input 

902 matrix using the Fourier collocation spectral method. The gradient 

903 for singleton dimensions is returned as 0. 

904 

905 Args: 

906 f: 

907 dn: 

908 dim: 

909 deriv_order: 

910 

911 Returns: 

912 

913 """ 

914 

915 # get size of the input function 

916 sz = f.shape 

917 

918 # check if input is 1D or user defined input dimension is given 

919 if dim or len(sz) == 1: 

920 

921 # check if a single dn value is given, if not, extract the required value 

922 if not (isinstance(dn, int) or isinstance(dn, float)): 

923 dn = dn[dim] 

924 

925 # get the grid size along the specified dimension, or the longest dimension if 1D 

926 if max(sz) == np.prod(sz): 

927 dim = np.argmax(sz) 

928 Nx = sz[dim] 

929 else: 

930 Nx = sz[dim] 

931 

932 # get the wave number 

933 kx = get_wave_number(Nx, dn, dim) 

934 

935 # calculate derivative and assign output 

936 grads = np.real(ifft((1j * kx) ** deriv_order * fft(f, axis=dim), axis=dim)) 

937 else: 

938 # warnings.warn("This implementation is not tested.") 

939 # get the wave number 

940 # kx = get_wave_number(sz(dim), dn[dim], dim) 

941 

942 assert len(dn) == len(sz), ValueError( 

943 f"{len(sz)} values for dn must be specified for a {len(sz)}-dimensional input matrix.") 

944 

945 grads = [] 

946 # calculate the gradient for each non-singleton dimension 

947 for dim in range(num_dim(f)): 

948 # get the wave number 

949 kx = get_wave_number(sz[dim], dn[dim], dim) 

950 # calculate derivative and assign output 

951 kx = broadcast_axis(kx, num_dim(f), dim) 

952 grads.append(np.real(ifft((1j * kx) ** deriv_order * fft(f, axis=dim), axis=dim))) 

953 

954 return grads 

955 

956 

957def unmask_sensor_data(kgrid, sensor, sensor_data: np.ndarray) -> np.ndarray: 

958 # create an empty matrix 

959 if kgrid.k == 1: 

960 unmasked_sensor_data = np.zeros((kgrid.Nx, 1)) 

961 elif kgrid.k == 2: 

962 unmasked_sensor_data = np.zeros((kgrid.Nx, kgrid.Ny)) 

963 elif kgrid.k == 3: 

964 unmasked_sensor_data = np.zeros((kgrid.Nx, kgrid.Ny, kgrid.Nz)) 

965 else: 

966 raise NotImplementedError 

967 

968 # reorder input data 

969 flat_sensor_mask = (sensor.mask != 0).flatten('F') 

970 assignment_mask = unflatten_matlab_mask(unmasked_sensor_data, np.where(flat_sensor_mask)[0]) 

971 # unmasked_sensor_data.flatten('F')[flat_sensor_mask] = sensor_data.flatten() 

972 unmasked_sensor_data[assignment_mask] = sensor_data.flatten() 

973 # unmasked_sensor_data[unflatten_matlab_mask(unmasked_sensor_data, sensor.mask != 0)] = sensor_data 

974 return unmasked_sensor_data 

975 

976 

977def reorder_sensor_data(kgrid, sensor, sensor_data: np.ndarray) -> np.ndarray: 

978 # check simulation is 2D 

979 if kgrid.dim != 2: 

980 raise ValueError('The simulation must be 2D.') 

981 

982 # check sensor.mask is a binary mask 

983 if sensor.mask.dtype != bool and set(np.unique(sensor.mask).tolist()) != {0, 1}: 

984 raise ValueError('The sensor must be defined as a binary mask.') 

985 

986 # find the coordinates of the sensor points 

987 x_sensor = matlab_mask(kgrid.x, sensor.mask == 1) 

988 x_sensor = np.squeeze(x_sensor) 

989 y_sensor = matlab_mask(kgrid.y, sensor.mask == 1) 

990 y_sensor = np.squeeze(y_sensor) 

991 

992 # find the angle of each sensor point (from the centre) 

993 angle = np.arctan2(-x_sensor, -y_sensor) 

994 angle[angle < 0] = 2 * np.pi + angle[angle < 0] 

995 

996 # sort the sensor points in order of increasing angle 

997 indices_new = np.argsort(angle) 

998 # [angle_sorted, indices_new] = sort(angle, 'ascend'); %#ok<ASGLU> 

999 

1000 # reorder the measure time series so that adjacent time series correspond 

1001 # to adjacent sensor points. 

1002 reordered_sensor_data = sensor_data[indices_new] 

1003 return reordered_sensor_data