Coverage for kwave/utils/conversionutils.py: 14%

1 import math

2 from math import floor

3 from typing import Optional, Tuple

4

5 import numpy as np

6 from scipy.interpolate import interp1d

7

8 from kwave.utils.tictoc import TicToc

9

10

11 def scale_time(seconds):

12 # switch to calculating years, weeks, and days if larger than 100 hours

13 if seconds > (60 * 60 * 100):

14 years = floor(seconds / (60 * 60 * 24 * 365))

15 seconds = seconds - years * 60 * 60 * 24 * 365

16 weeks = floor(seconds / (60 * 60 * 24 * 7))

17 seconds = seconds - weeks * 60 * 60 * 24 * 7

18 days = floor(seconds / (60 * 60 * 24))

19 seconds = seconds - days * 60 * 60 * 24

20 else:

21 years = 0

22 weeks = 0

23 days = 0

24

25 # calculate hours, minutes, and seconds

26 hours = floor(seconds / (60 * 60))

27 seconds = seconds - hours * 60 * 60

28 minutes = floor(seconds / 60)

29 seconds = seconds - minutes * 60

30

31 # write out as a string, to keep the output manageable, only the largest

32 # three units are written

33 if years > 0:

34 time = f'{years} years, {weeks} weeks, and {days} days'

35 elif weeks > 0:

36 time = f'{weeks} weeks, {days} days, and {hours} hours'

37 elif days > 0:

38 time = f'{days} days, {hours} hours, and {minutes} min'

39 elif hours > 0:

40 seconds = np.round(seconds, 4)

41 if np.abs(seconds - int(seconds)) < 1e-4:

42 seconds = int(seconds)

43 time = f'{hours}hours {minutes}min {seconds}s'

44 elif minutes > 0:

45 seconds = np.round(seconds, 4)

46 if np.abs(seconds - int(seconds)) < 1e-4:

47 seconds = int(seconds)

48 time = f'{minutes}min {seconds}s'

49 else:

50 precision = 10 # manually tuned number

51 seconds = round(seconds, precision)

52 time = f'{seconds}s'

53 return time

54

55

56 def scale_SI(x):

57 # force the input to be a scalar

58 x = np.max(x)

59

60 # check for a negative input

61 if x < 0:

62 x = -x

63 negative = True

64 else:

65 negative = False

66

67 if x == 0:

68

69 # if x is zero, don't scale

70 x_sc = x

71 prefix = ''

72 prefix_fullname = ''

73 scale = 1

74

75 elif x < 1:

76

77 # update index and input

78 x_sc = x * 1e3

79 sym_index = 1

80

81 # find scaling parameter

82 while x_sc < 1 and sym_index < 8:

83 x_sc = x_sc * 1e3

84 sym_index = sym_index + 1

85

86 # define SI unit scalings

87 units = {

88 1: ('m', 'milli', 1e3),

89 2: ('u', 'micro', 1e6),

90 3: ('n', 'nano', 1e9),

91 4: ('p', 'pico', 1e12),

92 5: ('f', 'femto', 1e15),

93 6: ('a', 'atto', 1e18),

94 7: ('z', 'zepto', 1e21),

95 8: ('y', 'yocto', 1e24),

96 }

97 prefix, prefix_fullname, scale = units[sym_index]

98

99 elif x >= 1000:

100

101 # update index and input

102 x_sc = x * 1e-3

103 sym_index = 1

104

105 # find scaling parameter

106 while x_sc >= 1000 and sym_index < 8:

107 x_sc = x_sc * 1e-3

108 sym_index = sym_index + 1

109

110 # define SI unit scalings

111 units = {

112 1: ('k', 'kilo', 1e-3),

113 2: ('M', 'mega', 1e-6),

114 3: ('G', 'giga', 1e-9),

115 4: ('T', 'tera', 1e-12),

116 5: ('P', 'peta', 1e-15),

117 6: ('E', 'exa', 1e-18),

118 7: ('Z', 'zetta', 1e-21),

119 8: ('Y', 'yotta', 1e-24)

120 }

121 prefix, prefix_fullname, scale = units[sym_index]

122

123 else:

124 # if x is between 1 and 1000, don't scale

125 x_sc = x

126 prefix = ''

127 prefix_fullname = ''

128 scale = 1

129

130 # form scaling into a string

131 round_decimals = 6 # TODO this needs to be tuned

132 x_sc = x_sc.round(round_decimals)

133 if (x_sc - int(x_sc)) < (0.1 ** round_decimals):

134 # avoid values like X.0, instead have only X

135 x_sc = int(x_sc)

136 x_sc = f'-{x_sc}{prefix}' if negative else f'{x_sc}{prefix}'

137 return x_sc, scale, prefix, prefix_fullname

138

139

140 def db2neper(alpha, y=1):

141 """

142 DB2NEPER Convert decibels to nepers.

143

144 DESCRIPTION:

145 db2neper converts an attenuation coefficient in units of

146 dB / (MHz ^ y cm) to units of Nepers / ((rad / s) ^ y m).

147

148 USAGE:

149 alpha = db2neper(alpha)

150 alpha = db2neper(alpha, y)

151

152 INPUTS:

153 alpha - attenuation in dB / (MHz ^ y cm)

154

155 OPTIONAL INPUTS:

156 y - power law exponent(default=1)

157

158 OUTPUTS:

159 alpha - attenuation in Nepers / ((rad / s) ^ y m)

160

161 ABOUT:

162 author - Bradley Treeby

163 date - 27 th March 2009

164 last update - 4 th June 2017

165

166 This function is part of the k - Wave Toolbox(http: // www.k - wave.org)

167 Copyright(C) 2009 - 2017 Bradley Treeby

168

169 See also neper2db

170

171 This file is part of k - Wave.k - Wave is free software: you can

172 redistribute it and / or modify it under the terms of the GNU Lesser

173 General Public License as published by the Free Software Foundation,

174 either version 3 of the License, or (at your option) any later version.

175

176 k - Wave is distributed in the hope that it will be useful, but WITHOUT ANY

177 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS

178 FOR A PARTICULAR PURPOSE.See the GNU Lesser General Public License for

179 more details.

180

181 You should have received a copy of the GNU Lesser General Public License

182 along with k - Wave.If not, see < http:// www.gnu.org / licenses / >.

183

184 set default y value if not given by user

185 """

186

187 # calculate conversion

188 alpha = 100 * alpha * (1e-6 / (2 * math.pi)) ** y / (20 * np.log10(np.exp(1)))

189 return alpha

190

191

192 def neper2db(alpha, y=1):

193 """

194 NEPER2DB Convert nepers to decibels.

195

196 DESCRIPTION:

197 % neper2db converts an attenuation coefficient in units of

198 % Nepers / ((rad / s) ^ y m) to units of dB / (MHz ^ y cm).

199 %

200 % USAGE:

201 % alpha = neper2db(alpha)

202 % alpha = neper2db(alpha, y)

203 %

204 % INPUTS:

205 % alpha - attenuation in Nepers / ((rad / s) ^ y m)

206 %

207 % OPTIONAL INPUTS:

208 % y - power law exponent(default=1)

209 %

210 % OUTPUTS:

211 % alpha - attenuation in dB / (MHz ^ y cm)

212 %

213 % ABOUT:

214 % author - Bradley Treeby

215 % date - 3 rd December 2009

216 % last update - 7 th June 2017

217 %

218 % This function is part of the k - Wave Toolbox(http: // www.k - wave.org)

219 % Copyright(C) 2009 - 2017 Bradley Treeby

220 %

221 % See also db2neper

222

223 % This file is part of k - Wave.k - Wave is free software: you can

224 % redistribute it and / or modify it under the terms of the GNU Lesser

225 % General Public License as published by the Free Software Foundation,

226 % either version 3 of the License, or (at your option) any later version.

227 %

228 % k - Wave is distributed in the hope that it will be useful, but WITHOUT ANY

229 % WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS

230 % FOR A PARTICULAR PURPOSE.See the GNU Lesser General Public License for

231 % more details.

232 %

233 % You should have received a copy of the GNU Lesser General Public License

234 % along with k - Wave.If not, see < http:// www.gnu.org / licenses / >.

235 """

236

237 # calculate conversion

238 alpha = 20 * math.log10(math.exp(1)) * alpha * (2 * math.pi * 1e6) ** y / 100

239 return alpha

240

241

242 def cast_to_type(data, matlab_type: str):

243 if not isinstance(data, np.ndarray):

244 data = np.array(data)

245 type_map = {

246 'single': np.float32,

247 'double': np.float64,

248 'uint64': np.uint64,

249 'uint32': np.uint32,

250 'uint16': np.uint16,

251 }

252 return data.astype(type_map[matlab_type])

253

254

255 def scan_conversion(

256 scan_lines: np.ndarray,

257 steering_angles,

258 image_size: Tuple[float, float],

259 c0,

260 dt,

261 resolution: Optional[Tuple[int, int]]

262) -> np.ndarray:

263

264 if resolution is None:

265 resolution = (256, 256) # in pixels

266

267 x_resolution, y_resolution = resolution

268

269 # assign the inputs

270 x, y = image_size

271

272 # start the timer

273 TicToc.tic()

274

275 # update command line status

276 print('Computing ultrasound scan conversion...')

277

278 # extract a_line parameters

279 Nt = scan_lines.shape[1]

280

281 # calculate radius variable based on the sound speed in the medium and the

282 # round trip distance

283 r = c0 * np.arange(1, Nt + 1) * dt / 2 # [m]

284

285 # create regular Cartesian grid to remap to

286 pos_vec_y_new = np.linspace(0, 1, y_resolution) * y - y / 2

287 pos_vec_x_new = np.linspace(0, 1, x_resolution) * x

288 [pos_mat_x_new, pos_mat_y_new] = np.array(np.meshgrid(pos_vec_x_new, pos_vec_y_new, indexing='ij'))

289

290 # convert new points to polar coordinates

291 [th_cart, r_cart] = cart2pol(pos_mat_x_new, pos_mat_y_new)

292

293 # TODO: move this import statement at the top of the file

294 # Not possible now due to cyclic dependencies

295 from kwave.utils.interputils import interpolate2D_with_queries

296

297 # below part has some modifications

298 # we flatten the _cart matrices and build queries

299 # then we get values at the query locations

300 # and reshape the values to the desired size

301 # These three steps can be accomplished in one step in Matlab

302 # However, we don't want to add custom logic to the `interpolate2D_with_queries` method.

303

304 # Modifications -start

305 queries = np.array([r_cart.flatten(), th_cart.flatten()]).T

306

307 b_mode = interpolate2D_with_queries(

308 [r, 2 * np.pi * steering_angles / 360],

309 scan_lines.T,

310 queries,

311 method='linear',

312 copy_nans=False

313 )

314 image_size_points = (len(pos_vec_x_new), len(pos_vec_y_new))

315 b_mode = b_mode.reshape(image_size_points)

316 # Modifications -end

317

318 b_mode[np.isnan(b_mode)] = 0

319

320 # update command line status

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

322

323 return b_mode

324

325

326 def cart2pol(x, y):

327 rho = np.sqrt(x**2 + y**2)

328 phi = np.arctan2(y, x)

329 return phi, rho

330

331

332 def revolve2D(mat2D):

333 # start timer

334 TicToc.tic()

335

336 # update command line status

337 print('Revolving 2D matrix to form a 3D matrix...')

338

339 # get size of matrix

340 m, n = mat2D.shape

341

342 # create the reference axis for the 2D image

343 r_axis_one_sided = np.arange(0, n)

344 r_axis_two_sided = np.arange(-(n-1), n)

345

346 # compute the distance from every pixel in the z-y cross-section of the 3D

347 # matrix to the rotation axis

348 z, y = np.meshgrid(r_axis_two_sided, r_axis_two_sided)

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

350

351 # create empty image matrix

352 mat3D = np.zeros((m, 2 * n - 1, 2 * n - 1))

353

354 # loop through each cross section and create 3D matrix

355 for x_index in range(m):

356 interp = interp1d(x=r_axis_one_sided, y=mat2D[x_index, :], kind='linear', bounds_error=False, fill_value=0)

357 mat3D[x_index, :, :] = interp(r)

358

359 # update command line status

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

361 return mat3D