Coverage for kwave/utils/matrixutils.py: 20%

1 from typing import Tuple

2 from skimage.transform import resize as si_resize

3 import numpy as np

4 import warnings

5

6 from .tictoc import TicToc

7 from .conversionutils import scale_time

8 from .checkutils import num_dim2, is_number

9 from .interputils import interpolate2D

10

11

12 def expand_matrix(matrix, exp_coeff, edge_val=None):

13 """

14 Enlarge a matrix by extending the edge values.

15

16 expandMatrix enlarges an input matrix by extension of the values at

17 the outer faces of the matrix (endpoints in 1D, outer edges in 2D,

18 outer surfaces in 3D). Alternatively, if an input for edge_val is

19 given, all expanded matrix elements will have this value. The values

20 for exp_coeff are forced to be real positive integers (or zero).

21

22 Note, indexing is done inline with other k-Wave functions using

23 mat(x) in 1D, mat(x, y) in 2D, and mat(x, y, z) in 3D.

24 Args:

25 matrix: the matrix to enlarge

26 exp_coeff: the number of elements to add in each dimension

27 in 1D: [a] or [x_start, x_end]

28 in 2D: [a] or [x, y] or

29 [x_start, x_end, y_start, y_end]

30 in 3D: [a] or [x, y, z] or

31 [x_start, x_end, y_start, y_end, z_start, z_end]

32 (here 'a' is applied to all dimensions)

33 edge_val: value to use in the matrix expansion

34 Returns:

35 expanded matrix

36 """

37 opts = {}

38 matrix = np.squeeze(matrix)

39

40 if edge_val is None:

41 opts['mode'] = 'edge'

42 else:

43 opts['mode'] = 'constant'

44 opts['constant_values'] = edge_val

45

46 exp_coeff = np.array(exp_coeff).astype(int).squeeze()

47 n_coeff = exp_coeff.size

48 assert n_coeff > 0

49

50 if n_coeff == 1:

51 opts['pad_width'] = exp_coeff

52 elif len(matrix.shape) == 1:

53 assert n_coeff <= 2

54 opts['pad_width'] = exp_coeff

55 elif len(matrix.shape) == 2:

56 if n_coeff == 2:

57 opts['pad_width'] = exp_coeff

58 if n_coeff == 4:

59 opts['pad_width'] = [(exp_coeff[0], exp_coeff[1]), (exp_coeff[2], exp_coeff[3])]

60 elif len(matrix.shape) == 3:

61 if n_coeff == 3:

62 opts['pad_width'] = np.tile(np.expand_dims(exp_coeff, axis=-1), [1, 2])

63 if n_coeff == 6:

64 opts['pad_width'] = [(exp_coeff[0], exp_coeff[1]), (exp_coeff[2], exp_coeff[3]), (exp_coeff[4], exp_coeff[5])]

65

66 return np.pad(matrix, **opts)

67

68

69 def matlab_find(arr, val=0, mode='neq'):

70 if not isinstance(arr, np.ndarray):

71 arr = np.array(arr)

72 if mode == 'neq':

73 arr = np.where(arr.flatten(order='F') != val)[0] + 1 # +1 due to matlab indexing

74 else: # 'eq'

75 arr = np.where(arr.flatten(order='F') == val)[0] + 1 # +1 due to matlab indexing

76 return np.expand_dims(arr, -1) # compatibility, n => [n, 1]

77

78

79 def matlab_mask(arr, mask, diff=None):

80 if diff is None:

81 return np.expand_dims(arr.ravel(order='F')[mask.ravel(order='F')], axis=-1) # compatibility, n => [n, 1]

82 else:

83 return np.expand_dims(arr.ravel(order='F')[mask.ravel(order='F') + diff], axis=-1) # compatibility, n => [n, 1]

84

85

86 def unflatten_matlab_mask(arr, mask, diff=None):

87 if diff is None:

88 return np.unravel_index(mask.ravel(order='F'), arr.shape, order='F')

89 else:

90 return np.unravel_index(mask.ravel(order='F') + diff, arr.shape, order='F')

91

92 def matlab_assign(matrix: np.ndarray, indices, values):

93 original_shape = matrix.shape

94 matrix = matrix.flatten(order='F')

95 matrix[indices] = values

96 return matrix.reshape(original_shape, order='F')

97

98# def _resize1D():

99# # extract the original number of pixels from the size of the matrix

100# [Nx_input, Ny_input] = size(mat);

101#

102# # extract the desired number of pixels

103# if Ny_input == 1

104# Nx_output = varargin{2}(1);

105# Ny_output = 1;

106# else

107# Nx_output = 1;

108# Ny_output = varargin{2}(1);

109# end

110#

111# # update command line status

112# disp([' input grid size: ' num2str(Nx_input) ' by ' num2str(Ny_input) ' elements']);

113# disp([' output grid size: ' num2str(Nx_output) ' by ' num2str(Ny_output) ' elements']);

114#

115# # check the size is different to the input size

116# if Nx_input ~= Nx_output || Ny_input ~= Ny_output

117#

118# # resize the input matrix to the desired number of pixels

119# if Ny_input == 1

120# mat_rs = interp1((0:1/(Nx_input - 1):1)', mat, (0:1/(Nx_output - 1):1)', interp_mode);

121# else

122# mat_rs = interp1((0:1/(Ny_input - 1):1), mat, (0:1/(Ny_output - 1):1), interp_mode);

123# end

124#

125# else

126# mat_rs = mat;

127# end

128# pass

129

130 def _resize2D(mat,new_size, interp_mode='linear'):

131 """

132 2D specification of resize method

133

134 Args:

135 mat:

136 new_size:

137 interp_mode:

138

139 Returns:

140 mat_rs:

141 """

142

143 # extract the original number of pixels from the size of the matrix

144 Nx_input, Ny_input = mat.shape

145

146 # extract the desired number of pixels

147 Nx_output, Ny_output = new_size

148

149 # update command line status

150 print(f' input grid size: {Nx_input} by {Ny_input} elements')

151 print(f' output grid size: {Nx_output} by {Ny_output} elements')

152

153 # check the size is different to the input size

154 if Nx_input != Nx_output or Ny_input != Ny_output:

155

156 # resize the input matrix to the desired number of pixels

157 inp_y = np.arange(0, 1 + 1e-8, 1 / (Ny_input - 1))

158 inp_x = np.arange(0, 1 + 1e-8, 1 / (Nx_input - 1))

159

160 out_y = np.arange(0, 1 + 1e-8, 1 / (Ny_output - 1))

161 out_x = np.arange(0, 1 + 1e-8, 1 / (Nx_output - 1))

162

163 mat_rs = interpolate2D([inp_x, inp_y], mat, [out_x, out_y], method=interp_mode, copy_nans=False)

164 print(mat_rs.shape)

165

166 # mat_rs = interp2(0:1/(Ny_input - 1):1, (0:1/(Nx_input - 1):1)', mat, 0:1/(Ny_output - 1):1, (0:1/(Nx_output - 1):1)', interp_mode);

167 else:

168 mat_rs = mat

169 return mat_rs

170

171# def _resize3D(mat, resolution, interp_mode='linear'):

172# # extract the original number of pixels from the size of the matrix

173# [Nx_input, Ny_input, Nz_input] = mat.shape

174#

175# # extract the desired number of pixels

176# Nx_output, Ny_output, Nz_output = resolution

177#

178# # update command line status

179# print(f' input grid size: {Nx_input} by {Ny_input} by {Nz_input} elements')

180# print(f' output grid size: {Nx_output} by {Ny_output} by {Nz_output} elements')

181#

182# # create normalised plaid grids of current discretisation

183# [x_mat, y_mat, z_mat] = ndgrid((0:Nx_input-1)/(Nx_input-1), (0:Ny_input-1)/(Ny_input-1), (0:Nz_input-1)/(Nz_input-1));

184#

185# # create plaid grids of desired discretisation

186# [x_mat_interp, y_mat_interp, z_mat_interp] = ndgrid((0:Nx_output-1)/(Nx_output-1), (0:Ny_output-1)/(Ny_output-1), (0:Nz_output-1)/(Nz_output-1));

187#

188# # compute interpolation; for a matrix indexed as [M, N, P], the

189# # axis variables must be given in the order N, M, P

190# mat_rs = interp3(y_mat, x_mat, z_mat, mat, y_mat_interp, x_mat_interp, z_mat_interp, interp_mode);

191# pass

192

193

194 def resize(mat, new_size, interp_mode='linear'):

195 """

196 resize: resamples a "matrix" of spatial samples to a desired "resolution" or spatial sampling frequency via interpolation

197

198 Args:

199 mat: matrix to be "resized" i.e. resampled

200 new_size: desired output resolution

201 interp_mode: interpolation method

202

203 Returns:

204 res_mat: "resized" matrix

205

206 """

207 # TODO: wrap scikit image resize

208 # https://scikit-image.org/docs/dev/api/skimage.transform.html#skimage.transform.resize

209 # start the timer

210 TicToc.tic()

211

212 # update command line status

213 print('Resizing matrix...')

214

215 # check inputs

216 assert num_dim2(mat) == len(new_size), \

217 'Resolution input must have the same number of elements as data dimensions.'

218

219 mat_rs = np.squeeze(si_resize(mat, new_size, order=0))

220 # if num_dim2(mat) == 1:

221 # raise NotImplementedError

222 # elif num_dim2(mat) == 2:

223 # mat_rs = _resize2D(mat, new_size, interp_mode)

224 # elif num_dim2(mat) == 3:

225 # raise NotImplementedError

226 # else:

227 # raise ValueError('Input matrix must be 1, 2 or 3 dimensional.')

228

229 # update command line status

230 print(f' completed in {scale_time(TicToc.toc())}')

231 assert mat_rs.shape == tuple(new_size)

232 return mat_rs

233

234

235 def smooth(mat, restore_max=False, window_type='Blackman'):

236 """

237 Smooth a matrix

238 Returns:

239

240 """

241 DEF_USE_ROTATION = True

242

243 assert is_number(mat) and np.all(~np.isinf(mat))

244 assert isinstance(restore_max, bool)

245 assert isinstance(window_type, str)

246

247 # get the grid size

248 grid_size = mat.shape

249

250 # remove singleton dimensions

251 if num_dim2(mat) != len(grid_size):

252 grid_size = np.squeeze(grid_size)

253

254 # use a symmetric filter for odd grid sizes, and a non-symmetric filter for

255 # even grid sizes to ensure the DC component of the window has a value of

256 # unity

257 window_symmetry = (np.array(grid_size) % 2).astype(bool)

258

259 # get the window, taking the absolute value to discard machine precision

260 # negative values

261 from .kutils import get_win

262 win, _ = get_win(grid_size, type_=window_type,

263 rotation=DEF_USE_ROTATION, symmetric=window_symmetry)

264 win = np.abs(win)

265

266 # rotate window if input mat is (1, N)

267 if mat.shape[0] == 1: # is row?

268 win = win.T

269

270 # apply the filter

271 mat_sm = np.real(np.fft.ifftn(np.fft.fftn(mat) * np.fft.ifftshift(win)))

272

273 # restore magnitude if required

274 if restore_max:

275 mat_sm = (np.abs(mat).max() / np.abs(mat_sm).max()) * mat_sm

276 return mat_sm

277

278

279 def gradient_FD(f, dx=None, dim=None, deriv_order=None, accuracy_order=None):

280 """

281 A wrapper of the numpy gradient method for use in the k-wave library.

282

283 gradient_FD calculates the gradient of an n-dimensional input matrix

284 using the finite-difference method. For one-dimensional inputs, the

285 gradient is always computed along the non-singleton dimension. For

286 higher dimensional inputs, the gradient for singleton dimensions is

287 returned as 0. For elements in the centre of the grid, the gradient

288 is computed using centered finite-differences. For elements on the

289 edge of the grid, the gradient is computed using forward or backward

290 finite-differences. The order of accuracy of the finite-difference

291 approximation is controlled by accuracy_order (default = 2). The

292 calculations are done using sparse multiplication, so the input

293 matrix is always cast to double precision.

294

295 Args:

296 f:

297 dx: array of values for the grid point spacing in each

298 dimension. If a value for dim is given, dn is the

299 spacing in dimension dim.

300 dim: optional input to specify a single dimension over which to compute the gradient for

301 n-dimension input functions

302 deriv_order: order of the derivative to compute, e.g., use 1 to

303 compute df/dx, 2 to compute df^2/dx^2, etc.

304 (default = 1)

305 accuracy_order: order of accuracy for the finite difference

306 coefficients. Because centered differences are

307 used, this must be set to an integer multiple of

308 2 (default = 2)

309

310 Returns:

311 fx, fy, ... gradient

312

313 """

314 if deriv_order:

315 warnings.warn("deriv_order is no longer a supported argument.", DeprecationWarning)

316 if accuracy_order:

317 warnings.warn("accuracy_order is no longer a supported argument.", DeprecationWarning)

318

319 if dim is not None and dx is not None:

320 return np.gradient(f, dx, axis=dim)

321 elif dim is not None:

322 return np.gradient(f, axis=dim)

323 elif dx is not None:

324 return np.gradient(f, dx)

325 else:

326 return np.gradient(f)

327

328

329 def min_nd(matrix: np.ndarray) -> Tuple[float, Tuple]:

330 min_val, linear_index = np.min(matrix), matrix.argmin()

331 numpy_index = np.unravel_index(linear_index, matrix.shape)

332 matlab_index = tuple(idx + 1 for idx in numpy_index)

333 return min_val, matlab_index

334

335

336 def max_nd(matrix: np.ndarray) -> Tuple[float, Tuple]:

337 max_val, linear_index = np.max(matrix), matrix.argmax()

338 numpy_index = np.unravel_index(linear_index, matrix.shape)

339 matlab_index = tuple(idx + 1 for idx in numpy_index)

340 return max_val, matlab_index