Coverage for kwave/utils/conversion.py: 12%

214 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2025-03-24 12:06 -0700

1import logging 

2import math 

3from typing import Any 

4 

5import numpy as np 

6from beartype import beartype as typechecker 

7from beartype.typing import Tuple, Union 

8from jaxtyping import Float, Num, Real 

9from numpy import ndarray 

10 

11import kwave.utils.typing as kt 

12from kwave.kgrid import kWaveGrid 

13from kwave.utils.matlab import matlab_mask 

14from kwave.utils.matrix import sort_rows 

15 

16 

17@typechecker 

18def db2neper(alpha: Real[kt.ArrayLike, "..."], y: Real[kt.ScalarLike, ""] = 1) -> Real[kt.ArrayLike, "..."]: 

19 """ 

20 Convert decibels to nepers. 

21 

22 Args: 

23 alpha: Attenuation in dB / (MHz ^ y cm). 

24 y: Power law exponent (default=1). 

25 

26 Returns: 

27 Attenuation in Nepers / ((rad / s) ^ y m). 

28 

29 """ 

30 

31 # calculate conversion 

32 alpha = 100.0 * alpha * (1e-6 / (2.0 * math.pi)) ** y / (20.0 * np.log10(np.exp(1))) 

33 return alpha 

34 

35 

36@typechecker 

37def neper2db(alpha: Real[kt.ArrayLike, "..."], y: Real[kt.ScalarLike, ""] = 1) -> Real[kt.ArrayLike, "..."]: 

38 """ 

39 Converts an attenuation coefficient in units of Nepers / ((rad / s) ^ y m) to units of dB / (MHz ^ y cm). 

40 

41 Args: 

42 alpha: Attenuation in Nepers / ((rad / s) ^ y m) 

43 y: Power law exponent (default=1) 

44 

45 Returns: 

46 Attenuation in dB / (MHz ^ y cm) 

47 

48 """ 

49 

50 # calculate conversion 

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

52 return alpha 

53 

54 

55@typechecker 

56def cast_to_type(data: Real[kt.ArrayLike, "..."], matlab_type: str) -> Any: 

57 """ 

58 

59 Args: 

60 data: The data to cast. 

61 matlab_type: The type to cast to. 

62 

63 Returns: 

64 The cast data. 

65 

66 """ 

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

68 data = np.array(data) 

69 type_map = { 

70 "single": np.float32, 

71 "double": np.float64, 

72 "uint64": np.uint64, 

73 "uint32": np.uint32, 

74 "uint16": np.uint16, 

75 } 

76 return data.astype(type_map[matlab_type]) 

77 

78 

79@typechecker 

80def cart2pol(x: Real[kt.ArrayLike, "..."], y: Real[kt.ArrayLike, "..."]) -> Tuple[Real[kt.ArrayLike, "..."], Real[kt.ArrayLike, "..."]]: 

81 """ 

82 Convert from cartesian to polar coordinates. 

83 

84 Args: 

85 x: The x-coordinate of the point. 

86 y: The y-coordinate of the point. 

87 

88 Returns: 

89 A tuple containing the polar coordinates of the point. 

90 

91 """ 

92 

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

94 phi = np.arctan2(y, x) 

95 return phi, rho 

96 

97 

98@typechecker 

99def grid2cart(input_kgrid: kWaveGrid, grid_selection: ndarray) -> Tuple[ndarray, ndarray]: 

100 """ 

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

102 

103 Args: 

104 input_kgrid: k-Wave grid object returned by kWaveGrid 

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

106 

107 Returns: 

108 cart_data: 1 x N, 2 x N, or 3 x N (for 1, 2, and 3 dimensions) array of Cartesian sensor points 

109 order_index: returns a list of indices of the returned cart_data coordinates. 

110 

111 Raises: 

112 ValueError: when input_kgrid.dim is not in [1, 2, 3] 

113 

114 """ 

115 

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

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

118 

119 if input_kgrid.dim > 0: 

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

121 if input_kgrid.dim > 1: 

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

123 if input_kgrid.dim > 2: 

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

125 if 0 <= input_kgrid.dim > 3: 

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

127 

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

129 return cart_data.squeeze(), order_index 

130 

131 

132@typechecker 

133def freq2wavenumber(n: int, k_max: float, filter_cutoff: float, c: float, k_dim: Union[int, Tuple[int]]) -> Tuple[int, float]: 

134 """ 

135 Convert the given frequency and maximum wavenumber to a wavenumber cutoff and filter size. 

136 

137 Args: 

138 n: The size of the grid. 

139 k_max: The maximum wavenumber. 

140 filter_cutoff: The frequency to convert to a wavenumber cutoff. 

141 c: The speed of sound. 

142 k_dim: The dimensions of the wavenumber grid. 

143 

144 Returns: 

145 A tuple containing the calculated filter size and wavenumber cutoff. 

146 

147 """ 

148 

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

150 

151 # set the alpha_filter size 

152 filter_size = round(n * k_cutoff / k_dim[-1]) 

153 

154 # check the alpha_filter size 

155 if filter_size > n: 

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

157 filter_size = n 

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

159 return filter_size, filter_cutoff 

160 

161 

162@typechecker 

163def cart2grid( 

164 kgrid: kWaveGrid, 

165 cart_data: Union[Float[ndarray, "1 NumPoints"], Float[ndarray, "2 NumPoints"], Float[ndarray, "3 NumPoints"]], 

166 axisymmetric: bool = False, 

167) -> Tuple: 

168 """ 

169 Interpolates the set of Cartesian points defined by 

170 cart_data onto a binary matrix defined by the kWaveGrid object 

171 kgrid using nearest neighbour interpolation. An error is returned if 

172 the Cartesian points are outside the computational domain defined by 

173 kgrid. 

174 

175 Args: 

176 kgrid: simulation grid 

177 cart_data: Cartesian sensor points 

178 axisymmetric: set to True to use axisymmetric interpolation 

179 

180 Returns: 

181 A binary grid 

182 

183 """ 

184 

185 # check for axisymmetric input 

186 if axisymmetric and kgrid.dim != 2: 

187 raise AssertionError("Axisymmetric flag only supported in 2D.") 

188 

189 # detect whether the inputs are for one, two, or three dimensions 

190 if kgrid.dim == 1: 

191 # one-dimensional 

192 data_x = cart_data[0, :] 

193 

194 # scale position values to grid centered pixel coordinates using 

195 # nearest neighbour interpolation 

196 data_x = np.round(data_x / kgrid.dx).astype(int) 

197 

198 # shift pixel coordinates to coincide with matrix indexing 

199 data_x = data_x + np.floor(kgrid.Nx // 2).astype(int) 

200 

201 # check if the points all lie within the grid 

202 if data_x.max() > kgrid.Nx or data_x.min() < 1: 

203 raise AssertionError("Cartesian points must lie within the grid defined by kgrid.") 

204 

205 # create empty grid 

206 grid_data = np.zeros((kgrid.Nx, 1)) 

207 

208 # create index variable 

209 point_index = np.arange(1, data_x.size + 1) 

210 

211 # map values 

212 for data_index in range(data_x.size): 

213 grid_data[data_x[data_index]] = point_index[data_index] 

214 

215 # extract reordering index 

216 reorder_index = np.reshape(grid_data[grid_data != 0], (-1, 1)) 

217 

218 elif kgrid.dim == 2: 

219 # two-dimensional 

220 data_x = cart_data[0, :] 

221 data_y = cart_data[1, :] 

222 

223 # scale position values to grid centered pixel coordinates using 

224 # nearest neighbour interpolation 

225 data_x = np.round(data_x / kgrid.dx).astype(int) 

226 data_y = np.round(data_y / kgrid.dy).astype(int) 

227 

228 # shift pixel coordinates to coincide with matrix indexing (leave 

229 # y-direction = radial-direction if axisymmetric) 

230 data_x = data_x + np.floor(kgrid.Nx // 2).astype(int) 

231 if not axisymmetric: 

232 data_y = data_y + np.floor(kgrid.Ny // 2).astype(int) 

233 

234 # check if the points all lie within the grid 

235 if data_x.max() >= kgrid.Nx or data_y.max() >= kgrid.Ny or data_x.min() < 0 or data_y.min() < 0: 

236 raise AssertionError("Cartesian points must lie within the grid defined by kgrid.") 

237 

238 # create empty grid 

239 grid_data = -1 * np.ones((kgrid.Nx, kgrid.Ny)) 

240 

241 # map values 

242 for data_index in range(data_x.size): 

243 grid_data[data_x[data_index], data_y[data_index]] = int(data_index) 

244 

245 # extract reordering index 

246 reorder_index = grid_data.flatten(order="F")[grid_data.flatten(order="F") != -1] 

247 reorder_index = reorder_index[:, None] + 1 # [N] => [N, 1] 

248 

249 elif kgrid.dim == 3: 

250 # three dimensional 

251 data_x = cart_data[0, :] 

252 data_y = cart_data[1, :] 

253 data_z = cart_data[2, :] 

254 

255 # scale position values to grid centered pixel coordinates using 

256 # nearest neighbour interpolation 

257 data_x = np.round(data_x / kgrid.dx).astype(int) 

258 data_y = np.round(data_y / kgrid.dy).astype(int) 

259 data_z = np.round(data_z / kgrid.dz).astype(int) 

260 

261 # shift pixel coordinates to coincide with matrix indexing 

262 data_x = data_x + np.floor(kgrid.Nx // 2).astype(int) 

263 data_y = data_y + np.floor(kgrid.Ny // 2).astype(int) 

264 data_z = data_z + np.floor(kgrid.Nz // 2).astype(int) 

265 

266 # check if the points all lie within the grid 

267 assert ( 

268 0 <= data_x.min() 

269 and 0 <= data_y.min() 

270 and 0 <= data_z.min() 

271 and data_x.max() < kgrid.Nx 

272 and data_y.max() < kgrid.Ny 

273 and data_z.max() < kgrid.Nz 

274 ), "Cartesian points must lie within the grid defined by kgrid." 

275 

276 # create empty grid 

277 grid_data = -1 * np.ones((kgrid.Nx, kgrid.Ny, kgrid.Nz), dtype=int) 

278 

279 # create index variable 

280 point_index = np.arange(1, data_x.size + 1) 

281 

282 # map values 

283 for data_index in range(data_x.size): 

284 grid_data[data_x[data_index], data_y[data_index], data_z[data_index]] = point_index[data_index] 

285 

286 # extract reordering index 

287 reorder_index = grid_data.flatten(order="F")[grid_data.flatten(order="F") != -1] 

288 reorder_index = reorder_index[:, None, None] # [N] => [N, 1, 1] 

289 else: 

290 raise ValueError("Input cart_data must be a 1, 2, or 3 dimensional matrix.") 

291 

292 # compute the reverse ordering index (i.e., what is the index of each point 

293 # in the reordering vector) 

294 order_index = np.ones((reorder_index.size, 2), dtype=int) 

295 order_index[:, 0] = np.squeeze(reorder_index) 

296 order_index[:, 1] = np.arange(1, reorder_index.size + 1) 

297 order_index = sort_rows(order_index, 0) 

298 order_index = order_index[:, 1] 

299 order_index = order_index[:, None] # [N] => [N, 1] 

300 

301 # reset binary grid values 

302 if kgrid.dim == 1: 

303 grid_data[grid_data != 0] = 1 

304 else: 

305 grid_data[grid_data != -1] = 1 

306 grid_data[grid_data == -1] = 0 

307 

308 # check if any Cartesian points have been mapped to the same grid point, 

309 # thereby reducing the total number of points 

310 num_discarded_points = cart_data.shape[1] - np.sum(grid_data) 

311 if num_discarded_points != 0: 

312 logging.log(logging.INFO, f" cart2grid: {num_discarded_points} Cartesian points mapped to overlapping grid points") 

313 return grid_data.astype(int), order_index, reorder_index 

314 

315 

316@typechecker 

317def hounsfield2soundspeed( 

318 ct_data: Union[Float[ndarray, "Dim1 Dim2"], Float[ndarray, "Dim1 Dim2 Dim3"]], 

319) -> Union[Float[ndarray, "Dim1 Dim2"], Float[ndarray, "Dim1 Dim2 Dim3"]]: 

320 """ 

321 Calculates the sound speed of a medium given a CT (computed tomography) of the medium. 

322 For soft tissue, the approximate sound speed can also be returned using the empirical relationship 

323 given by Mast [1]. 

324 

325 Args: 

326 ct_data: matrix of Hounsfield values. 

327 

328 Returns: 

329 A matrix of sound speed values of size of ct_data. 

330 

331 References: 

332 [1] Mast, T. D., "Empirical relationships between acoustic parameters in human soft tissues," 

333 Acoust. Res. Lett. Online, 1(2), pp. 37-42 (2000). 

334 """ 

335 # calculate corresponding sound speed values if required using soft tissue relationship 

336 # TODO confirm that this linear relationship is correct 

337 sound_speed = (hounsfield2density(ct_data) + 349) / 0.893 

338 

339 return sound_speed 

340 

341 

342@typechecker 

343def hounsfield2density( 

344 ct_data: Union[Float[ndarray, "Dim1 Dim2"], Float[ndarray, "Dim1 Dim2 Dim3"]], plot_fitting: bool = False 

345) -> Union[Float[ndarray, "Dim1 Dim2"], Float[ndarray, "Dim1 Dim2 Dim3"]]: 

346 """ 

347 Convert Hounsfield units in CT data to density values [kg / m ^ 3] based on experimental data. 

348 

349 Args: 

350 ct_data: The CT data in Hounsfield units. 

351 plot_fitting (bool, optional): Whether to plot the fitting curve (default: False). 

352 

353 Returns: 

354 The density values in [kg / m ^ 3]. 

355 

356 """ 

357 

358 # create empty density matrix 

359 density = np.zeros(ct_data.shape, like=ct_data) 

360 

361 # apply conversion in several parts using linear fits to the data 

362 # Part 1: Less than 930 Hounsfield Units 

363 density[ct_data < 930] = np.polyval([1.025793065681423, -5.680404011488714], ct_data[ct_data < 930]) 

364 

365 # Part 2: Between 930 and 1098(soft tissue region) 

366 index_selection = np.logical_and(930 <= ct_data, ct_data <= 1098) 

367 density[index_selection] = np.polyval([0.9082709691264, 103.6151457847139], ct_data[index_selection]) 

368 

369 # Part 3: Between 1098 and 1260(between soft tissue and bone) 

370 index_selection = np.logical_and(1098 < ct_data, ct_data < 1260) 

371 density[index_selection] = np.polyval([0.5108369316599, 539.9977189228704], ct_data[index_selection]) 

372 

373 # Part 4: Greater than 1260(bone region) 

374 density[ct_data >= 1260] = np.polyval([0.6625370912451, 348.8555178455294], ct_data[ct_data >= 1260]) 

375 

376 if plot_fitting: 

377 raise NotImplementedError("Plotting function not implemented in Python") 

378 

379 return density 

380 

381 

382tol = None 

383subs0 = None 

384 

385 

386@typechecker 

387def tol_star( 

388 tolerance: kt.NUMERIC, 

389 kgrid: kWaveGrid, 

390 point: Union[Float[ndarray, "1"], Float[ndarray, "2"], Float[ndarray, "3"]], 

391 debug, 

392) -> Tuple[ndarray, ndarray, ndarray, ndarray]: 

393 global tol, subs0 

394 

395 ongrid_threshold = kgrid.dx * 1e-3 

396 

397 kgrid_dim = kgrid.dim 

398 if tol is None or tolerance != tol or len(subs0) != kgrid_dim: 

399 tol = tolerance 

400 

401 decay_subs = int(np.ceil(1 / (np.pi * tol))) 

402 

403 lin_ind = np.arange(-decay_subs, decay_subs + 1) 

404 

405 if kgrid_dim == 1: 

406 is0 = lin_ind 

407 elif kgrid_dim == 2: 

408 is0, js0 = np.meshgrid(lin_ind, lin_ind, indexing="ij") 

409 elif kgrid_dim == 3: 

410 is0, js0, ks0 = np.meshgrid(lin_ind, lin_ind, lin_ind, indexing="ij") 

411 

412 if kgrid_dim == 1: 

413 subs0 = [is0] 

414 elif kgrid_dim == 2: 

415 instar = np.abs(is0 * js0) <= decay_subs 

416 is0 = matlab_mask(is0, instar) 

417 js0 = matlab_mask(js0, instar) 

418 subs0 = [is0, js0] 

419 elif kgrid_dim == 3: 

420 instar = np.abs(is0 * js0 * ks0) <= decay_subs 

421 is0 = matlab_mask(is0, instar).T 

422 js0 = matlab_mask(js0, instar).T 

423 ks0 = matlab_mask(ks0, instar).T 

424 subs0 = [is0, js0, ks0] 

425 

426 is_ = subs0[0].copy() 

427 js = subs0[1].copy() if kgrid_dim > 1 else [] 

428 ks = subs0[2].copy() if kgrid_dim > 2 else [] 

429 

430 # returns python index value (0 start) not MATLAB index (1 start) 

431 x_closest, x_closest_ind = find_closest(kgrid.x_vec, point[0]) 

432 

433 if np.abs(x_closest - point[0]) < ongrid_threshold: 

434 if kgrid_dim > 1: 

435 js = js[is_ == 0] 

436 if kgrid_dim > 2: 

437 ks = ks[is_ == 0] 

438 is_ = is_[is_ == 0] 

439 

440 if kgrid_dim > 1: 

441 y_closest, y_closest_ind = find_closest(kgrid.y_vec, point[1]) 

442 if np.abs(y_closest - point[1]) < ongrid_threshold: 

443 is_ = is_[js == 0] 

444 if kgrid_dim > 2: 

445 ks = ks[js == 0] 

446 js = js[js == 0] 

447 

448 if kgrid_dim > 2: 

449 z_closest, z_closest_ind = find_closest(kgrid.z_vec, point[2]) 

450 if np.abs(z_closest - point[2]) < ongrid_threshold: 

451 is_ = is_[ks == 0] 

452 js = js[ks == 0] 

453 ks = ks[ks == 0] 

454 

455 # TODO: closest index is added to is_, +1 might not be correct 

456 is_ += x_closest_ind + 1 

457 if kgrid_dim > 1: 

458 js += y_closest_ind + 1 

459 if kgrid_dim > 2: 

460 ks += z_closest_ind + 1 

461 

462 if kgrid_dim == 1: 

463 inbounds = (1 <= is_) & (is_ <= kgrid.Nx) 

464 # TODO: this should likely be matlabmask and indexes should be checked. 

465 is_ = is_[inbounds] 

466 elif kgrid_dim == 2: 

467 inbounds = (1 <= is_) & (is_ <= kgrid.Nx) & (1 <= js) & (js <= kgrid.Ny) 

468 is_ = is_[inbounds] 

469 js = js[inbounds] 

470 if kgrid_dim == 3: 

471 inbounds = (1 <= is_) & (is_ <= kgrid.Nx) & (1 <= js) & (js <= kgrid.Ny) & (1 <= ks) & (ks <= kgrid.Nz) 

472 is_ = is_[inbounds] 

473 js = js[inbounds] 

474 ks = ks[inbounds] 

475 

476 if kgrid_dim == 1: 

477 lin_ind = is_ 

478 elif kgrid_dim == 2: 

479 lin_ind = kgrid.Nx * (js - 1) + is_ 

480 elif kgrid_dim == 3: 

481 lin_ind = kgrid.Nx * kgrid.Ny * (ks - 1) + kgrid.Nx * (js - 1) + is_ 

482 

483 # TODO: in current form linear indexing matches MATLAB, but might break in 0 indexed python 

484 return lin_ind, np.array(is_) - 1, np.array(js) - 1, np.array(ks) - 1 # -1 for mapping from Matlab indexing to Python indexing 

485 

486 

487@typechecker 

488def find_closest(array: ndarray, value: Num[kt.ScalarLike, ""]): 

489 array = np.asarray(array) 

490 idx = (np.abs(array - value)).argmin() 

491 return array[idx], idx 

492 

493 

494def create_index_at_dim(ndim: int, dim: int, index_value: Any) -> tuple: 

495 """ 

496 Create a tuple of slice objects with a specific index value at the specified dimension. 

497 

498 Args: 

499 ndim: Number of dimensions in the array 

500 dim: The dimension where the specific index should be placed 

501 index_value: The index value to place at the specified dimension 

502 

503 Returns: 

504 A tuple of slice objects with the index value at the specified dimension 

505 """ 

506 return tuple(index_value if i == dim else slice(None) for i in range(ndim))