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
« 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
7from kwave.utils.checkutils import num_dim
8from kwave.utils.conversionutils import scale_SI
10from kwave.kgrid import kWaveGrid
11import numpy as np
12import warnings
13import scipy
14from numpy.fft import ifftshift, fft, ifft, fftshift
16from .misc import sinc, ndgrid, gaussian
17from .conversionutils import db2neper
19import math
22def primefactors(n):
23 # even number divisible
24 factors = []
25 while n % 2 == 0:
26 factors.append(2),
27 n = n / 2
29 # n became odd
30 for i in range(3, int(math.sqrt(n)) + 1, 2):
32 while (n % i == 0):
33 factors.append(i)
34 n = n / i
36 if n > 2:
37 factors.append(n)
39 return factors
42def check_factors(min_number, max_number):
43 """
44 Return the maximum prime factor for a range of numbers.
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.
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
56 Returns:
58 """
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))
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])
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.
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
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)
113 # define literals
114 FIXED_POINT_ACCURACY = 1e-12
116 # find the maximum wavenumber
117 kmax = kgrid.k.max()
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 }
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)
139 # calculate the timesteps required for stability
140 if medium.alpha_coeff is None or np.all(medium.alpha_coeff == 0):
142 # =====================================================================
143 # NON-ABSORBING CASE
144 # =====================================================================
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')
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())
155 else:
157 # =====================================================================
158 # ABSORBING CASE
159 # =====================================================================
161 # convert the absorption coefficient to nepers.(rad/s)^-y.m^-1
162 medium.alpha_coeff = db2neper(medium.alpha_coeff, medium.alpha_power)
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])
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])
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)
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())
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
190 # first define the function to iterate
191 def kappa(dt):
192 return sinc(c_ref * kmax * dt / 2)
194 def temp3(dt):
195 return medium.sound_speed.max() * absorb_tau.min() * kappa(dt) * kmax ** (medium.alpha_power - 1)
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())
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)
208 return dt_stability_limit
211def add_noise(signal, snr, mode="rms"):
212 """
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'
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.
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.")
230 # calculate the standard deviation of the Gaussian noise
231 std_dev = reference / (10 ** (snr / 20))
233 # calculate noise
234 noise = std_dev * np.random.randn(*signal.shape)
236 # check the snr
237 noise_rms = np.sqrt(np.mean(noise ** 2))
238 snr = 20. * np.log10(reference / noise_rms)
240 # add noise to the recorded sensor data
241 signal = signal + noise
243 return signal
246def grid2cart(input_kgrid: kWaveGrid, grid_selection: ndarray):
247 """
248 Returns the Cartesian coordinates of the non-zero points of a binary grid.
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.
255 USAGE:
256 [cart_data, order_index] = grid2cart(kgrid, grid_data)
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
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)))
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.")
279 order_index = np.argwhere(grid_data.squeeze() != 0)
280 return cart_data.squeeze(), order_index
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).
323 param: Control parameter for the Tukey, Blackman, Gaussian,
324 and Kaiser windows:
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)
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.
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.
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.
348 Returns:
349 win: the window
350 cg: the coherent gain of the window
352 """
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:
362 Returns:
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
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)
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)
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)
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))
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
409 # create the window
410 if N.size == 1:
411 n = np.arange(0, N)
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_}')
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)
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 }
483 # create the 2D window
484 if rotation:
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)
491 # create the reference axis
492 radius = (L - 1) / 2
493 ll = np.linspace(-radius, radius, L)
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])
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)
510 # create the 2D window using the outer product
511 win = (win_y * win_x.T).T
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]]
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:
523 # create the window in one dimension using getWin recursively
524 L = N.max()
525 win_lin, _ = get_win(L, type_, param=param)
527 # create the reference axis
528 radius = (L - 1) / 2
529 ll = np.linspace(-radius, radius, L)
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
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])
544 else:
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)
551 # create the 2D window using the outer product
552 win_2D = (win_x * win_z.T)
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]
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]]
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.')
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
583 return win, cg
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.
609 'Envelope' - Envelope used to taper the tone burst. Valid inputs
610 are:
612 'Gaussian' (the default)
613 'Rectangular'
614 [num_ring_up_cycles, num_ring_down_cycles]
616 The last option generates a continuous wave signal
617 with a cosine taper of the specified length at the
618 beginning and end.
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.
628 Returns: created tone burst
630 """
631 assert isinstance(signal_offset, int), "signal_offset must be integer"
632 assert isinstance(signal_length, int), "signal_length must be integer"
634 # calculate the temporal spacing
635 dt = 1 / sample_freq # [s]
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)
645 tone_burst = np.sin(2 * np.pi * signal_freq * tone_t)
646 tone_index = round(signal_offset)
648 # check for ring up and ring down input
649 if isinstance(envelope, list) or isinstance(envelope, np.ndarray): # and envelope.size == 2:
651 # assign the inputs
652 num_ring_up_cycles, num_ring_down_cycles = envelope
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.'
658 # get period
659 period = 1 / signal_freq
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))
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
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
675 else:
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}.')
689 # apply the envelope
690 tone_burst = tone_burst * window
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])
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)
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])
709 for offset in range(tone_index.size):
710 signal[offset, tone_index[offset]:tone_index[offset] + len(tone_burst)] = tone_burst.T
712 # plot the signal if required
713 if plot_signal:
714 raise NotImplementedError
716 return signal
719def reorder_binary_sensor_data(sensor_data: np.ndarray, reorder_index: np.ndarray):
720 """
722 Args:
723 sensor_data: N x K
724 reorder_index: N
726 Returns:
728 """
729 reorder_index = np.squeeze(reorder_index)
730 assert sensor_data.ndim == 2
731 assert reorder_index.ndim == 1
733 return sensor_data[reorder_index.argsort()]
736def calc_max_freq(max_spat_freq, c):
737 filter_cutoff_freq = max_spat_freq * c / (2 * np.pi)
738 return filter_cutoff_freq
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:
750 Returns:
752 """
753 k_cutoff = 2 * np.pi * filter_cutoff / c
755 # set the alpha_filter size
756 filter_size = round(N * k_cutoff / k_dim[-1])
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
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.
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:
781 Returns:
782 alpha_filter:
783 """
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)
790 assert len(filter_cutoff) == dim, f"Input filter_cutoff must have {dim} elements for a {dim}D grid"
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)
803 # create the alpha_filter
804 filter_sec, _ = get_win(filter_size, 'Tukey', param=taper_ratio, rotation=True)
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))]
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
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] + '.')
822 return alpha_filter
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.
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
843 Returns:
844 input_signal_mat: matrix of time series following the source points
845 """
847 assert kgrid.t_array != 'auto', "kgrid.t_array must be defined."
848 if isinstance(sound_speed, int):
849 sound_speed = float(sound_speed)
851 assert isinstance(sound_speed, float), "sound_speed must be a scalar."
853 positions = [kgrid.x.flatten(), kgrid.y.flatten(), kgrid.z.flatten()]
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)
860 if isinstance(focus_position, list):
861 focus_position = np.array(focus_position)
862 assert isinstance(focus_position, np.ndarray)
864 dist = np.linalg.norm(positions[:, source_mask.flatten() == 1] - focus_position[:, np.newaxis])
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)
871 signal_mat = np.zeros((rel_delay.size, input_signal.size + max_delay))
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
877 warnings.warn("This method is not fully migrated, might be depricated and is untested.", PendingDeprecationWarning)
878 return signal_mat
881def broadcast_axis(data, ndims, axis):
882 newshape = [1] * ndims
883 newshape[axis] = -1
884 return data.reshape(*newshape)
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
894 kx = ifftshift((2 * np.pi / dx) * nx)
896 return kx
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.
905 Args:
906 f:
907 dn:
908 dim:
909 deriv_order:
911 Returns:
913 """
915 # get size of the input function
916 sz = f.shape
918 # check if input is 1D or user defined input dimension is given
919 if dim or len(sz) == 1:
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]
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]
932 # get the wave number
933 kx = get_wave_number(Nx, dn, dim)
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)
942 assert len(dn) == len(sz), ValueError(
943 f"{len(sz)} values for dn must be specified for a {len(sz)}-dimensional input matrix.")
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)))
954 return grads
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
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
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.')
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.')
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)
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]
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>
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