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

1 from copy import deepcopy

2 from math import floor

3 from typing import Union, List, Optional

4 from numpy import ndarray, array

5 from kwave.utils.matrixutils import unflatten_matlab_mask, matlab_mask

6

7 from kwave.utils.checkutils import num_dim

8 from kwave.utils.conversionutils import scale_SI

9

10 from kwave.kgrid import kWaveGrid

11 import numpy as np

12 import warnings

13 import scipy

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

15

16 from .misc import sinc, ndgrid, gaussian

17 from .conversionutils import db2neper

18

19 import math

20

21

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

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

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

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

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

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

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

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

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

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

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

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

881 def broadcast_axis(data, ndims, axis):

882 newshape = [1] * ndims

883 newshape[axis] = -1

884 return data.reshape(*newshape)

885

886

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

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

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

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