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

1021 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-10-24 11:55 -0700

1import math 

2from math import floor 

3from typing import Tuple, Optional 

4 

5import matplotlib.pyplot as plt 

6import numpy as np 

7from kwave.utils.tictoc import TicToc 

8 

9from kwave.utils.matrixutils import matlab_find, matlab_assign, max_nd 

10from kwave.utils.conversionutils import scale_SI 

11from scipy import optimize 

12import warnings 

13 

14from kwave.utils.conversionutils import db2neper, neper2db 

15 

16 

17def get_spaced_points(start, stop, n=100, spacing='linear'): 

18 """ 

19 getSpacedPoints generates a row vector of either logarithmically or 

20 linearly spaced points between X1 and X2. When spacing is set to 

21 'linear', the function is identical to the inbuilt np.linspace 

22 function. When spacing is set to 'log', the function is similar to 

23 the inbuilt np.logspace function, except that X1 and X2 define the start 

24 and end numbers, not decades. For logarithmically spaced points, X1 

25 must be > 0. If N < 2, X2 is returned. 

26 Args: 

27 start: 

28 stop: 

29 n: 

30 spacing: 

31 

32 Returns: 

33 points: 

34 """ 

35 # check if the end point is larger than the start point 

36 if stop <= start: 

37 raise ValueError('X2 must be larger than X1.') 

38 

39 if spacing == 'linear': 

40 return np.linspace(start, stop, num=n) 

41 elif spacing == 'log': 

42 return np.geomspace(start, stop, num=n) 

43 else: 

44 raise ValueError(f"spacing {spacing} is not a valid argument. Choose from 'linear' or 'log'.") 

45 

46 

47def pull_matlab(prop, expected_val=None): 

48 from scipy.io import loadmat 

49 

50 temp_mat = loadmat('/data/code/Work/black_box_testing/temp.mat') 

51 if expected_val is not None: 

52 return np.allclose(expected_val, temp_mat[prop]) 

53 return temp_mat[prop] 

54 

55 

56def fit_power_law_params(a0, y, c0, f_min, f_max, plot_fit=False): 

57 """ 

58 

59 fit_power_law_params calculates the absorption parameters that should 

60 be defined in the simulation functions given the desired power law 

61 absorption behaviour defined by a0 and y. This takes into account the 

62 actual absorption behaviour exhibited by the fractional Laplacian 

63 wave equation. 

64 

65 This fitting is required when using large absorption values or high 

66 frequencies, as the fractional Laplacian wave equation solved in 

67 kspaceFirstOrderND and kspaceSecondOrder no longer encapsulates 

68 absorption of the form a = a0*f^y. 

69 

70 The returned values should be used to define the medium.alpha_coeff 

71 and medium.alpha_power within the simulation functions. The 

72 absorption behaviour over the frequency range f_min:f_max will then 

73 follow the power law defined by a0 and y.Add testing for getOptimalPMLSize() 

74 

75 Args: 

76 a0: 

77 y: 

78 c0: 

79 f_min: 

80 f_max: 

81 plot_fit: 

82 

83 Returns: 

84 a0_fit: 

85 y_fit: 

86 

87 """ 

88 # define frequency axis 

89 f = get_spaced_points(f_min, f_max, 200) 

90 w = 2 * np.pi * f 

91 # convert user defined a0 to Nepers/((rad/s)^y m) 

92 a0_np = db2neper(a0, y) 

93 

94 desired_absorption = a0_np * w ** y 

95 

96 def abs_func(trial_vals): 

97 """Second-order absorption error""" 

98 a0_np_trial, y_trial = trial_vals 

99 

100 actual_absorption = a0_np_trial * w ** y_trial / (1 - (y_trial + 1) * \ 

101 a0_np_trial * c0 * np.tan(np.pi * y_trial / 2) * w ** ( 

102 y_trial - 1)) 

103 

104 absorption_error = np.sqrt(np.sum((desired_absorption - actual_absorption) ** 2)) 

105 

106 return absorption_error 

107 

108 a0_np_fit, y_fit = optimize.fmin(abs_func, [a0_np, y]) 

109 

110 a0_fit = neper2db(a0_np_fit, y_fit) 

111 

112 if plot_fit: 

113 raise NotImplementedError 

114 

115 return a0_fit, y_fit 

116 

117 

118def power_law_kramers_kronig(w, w0, c0, a0, y): 

119 """ 

120 POWERLAWKRAMERSKRONIG Calculate dispersion for power law absorption. 

121 

122 DESCRIPTION: 

123 powerLawKramersKronig computes the variation in sound speed for an 

124 attenuating medium using the Kramers-Kronig for power law 

125 attenuation where att = a0*w^y. The power law parameters must be in 

126 Nepers / m, with the frequency in rad/s. The variation is given 

127 about the sound speed c0 at a reference frequency w0. 

128 

129 USAGE: 

130 c_kk = power_law_kramers_kronig(w, w0, c0, a0, y) 

131 

132 INPUTS: 

133 

134 OUTPUTS: 

135 

136 Args: 

137 w: input frequency array [rad/s] 

138 w0: reference frequency [rad/s] 

139 c0: sound speed at w0 [m/s] 

140 a0: power law coefficient [Nepers/((rad/s)^y m)] 

141 y: power law exponent, where 0 < y < 3 

142 

143 Returns: 

144 c_kk variation of sound speed with w [m/s] 

145 

146 """ 

147 if 0 >= y or y >= 3: 

148 warnings.warn("y must be within the interval (0,3)", UserWarning) 

149 c_kk = c0 * np.ones_like(w) 

150 elif y == 1: 

151 # Kramers-Kronig for y = 1 

152 c_kk = 1 / (1 / c0 - 2 * a0 * np.log(w / w0) / np.pi) 

153 else: 

154 # Kramers-Kronig for 0 < y < 1 and 1 < y < 3 

155 c_kk = 1 / (1 / c0 + a0 * np.tan(y * np.pi / 2) * (w ** (y - 1) - w0 ** (y - 1))) 

156 

157 return c_kk 

158 

159 

160def water_absorption(f, temp): 

161 """ 

162 water_absorption Calculate ultrasound absorption in distilled water. 

163 

164 DESCRIPTION: 

165 waterAbsorption calculates the ultrasonic absorption in distilled 

166 water at a given temperature and frequency using a 7 th order 

167 polynomial fitted to the data given by np.pinkerton(1949). 

168 

169 USAGE: 

170 abs = waterAbsorption(f, T) 

171 

172 INPUTS: 

173 f - f frequency value [MHz] 

174 T - water temperature value [degC] 

175 

176 OUTPUTS: 

177 abs - absorption[dB / cm] 

178 

179 REFERENCES: 

180 [1] np.pinkerton(1949) "The Absorption of Ultrasonic Waves in Liquids 

181 and its Relation to Molecular Constitution, " Proceedings of the 

182 Physical Society.Section B, 2, 129 - 141 

183 

184 """ 

185 

186 NEPER2DB = 8.686 

187 # check temperature is within range 

188 if not 0 <= temp <= 60: 

189 raise Warning("Temperature outside range of experimental data") 

190 

191 # conversion factor between Nepers and dB NEPER2DB = 8.686; 

192 # coefficients for 7th order polynomial fit 

193 a = [56.723531840522710, -2.899633796917384, 0.099253401567561, -0.002067402501557, 2.189417428917596e-005, 

194 -6.210860973978427e-008, -6.402634551821596e-010, 3.869387679459408e-012] 

195 

196 # TODO: this is not a vectorized version of this function. This is different than the MATLAB version 

197 # make sure vectors are in the correct orientation 

198 # T = reshape(T, 3, []); 

199 # f = reshape(f, [], 1); 

200 

201 # compute absorption 

202 a_on_fsqr = (a[0] + a[1] * temp + a[2] * temp ** 2 + a[3] * temp ** 3 + a[4] * temp ** 4 + a[5] * temp ** 5 + a[ 

203 6] * temp ** 6 + a[7] * temp ** 7) * 1e-17 

204 

205 abs = NEPER2DB * 1e12 * f ** 2 * a_on_fsqr 

206 return abs 

207 

208 

209def hounsfield2soundspeed(ct_data): 

210 """ 

211 

212 Calclulates the soundspeed of a medium given a CT of the medium. 

213 For soft-tissue, the approximate sound speed can also be returned 

214 using the empirical relationship given by Mast. 

215 

216 Mast, T. D., "Empirical relationships between acoustic parameters 

217 in human soft tissues," Acoust. Res. Lett. Online, 1(2), pp. 37-42 

218 (2000). 

219 

220 Args: 

221 ct_data:  

222 

223 Returns: sound_speed: matrix of sound speed values of size of ct data 

224 

225 """ 

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

227 # TODO confirm that this linear relationship is correct 

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

229 

230 return sound_speed 

231 

232 

233def hounsfield2density(ct_data, plot_fitting=False): 

234 """ 

235 Convert Hounsfield units in CT data to density values [kg / m ^ 3] 

236 based on the experimental data given by Schneider et al. 

237 The conversion is made using a np.piece-wise linear fit to the data. 

238 

239 Args: 

240 ct_data: 

241 plot_fitting: 

242 

243 Returns: 

244 density_map: density in kg / m ^ 3 

245 """ 

246 # create empty density matrix 

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

248 

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

250 # Part 1: Less than 930 Hounsfield Units 

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

252 

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

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

255 density[index_selection] = np.polyval([0.9082709691264, 103.6151457847139], 

256 ct_data[index_selection]) 

257 

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

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

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

261 

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

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

264 

265 if plot_fitting: 

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

267 

268 return density 

269 

270 

271def water_sound_speed(temp): 

272 """ 

273 WATERSOUNDSPEED Calculate the sound speed in distilled water with temperature. 

274 

275 DESCRIPTION: 

276 waterSoundSpeed calculates the sound speed in distilled water at a 

277 a given temperature using the 5th order polynomial given by Marczak 

278 (1997). 

279 

280 USAGE: 

281 c = waterSoundSpeed(T) 

282 

283 INPUTS: 

284 T - water temperature in the range 0 to 95 [degC] 

285 

286 OUTPUTS: 

287 c - sound speed [m/s] 

288 

289 REFERENCES: 

290 [1] Marczak (1997) "Water as a standard in the measurements of speed 

291 of sound in liquids," J. Acoust. Soc. Am., 102, 2776-2779. 

292 

293 """ 

294 

295 # check limits 

296 assert 95 >= temp >= 0, "temp must be between 0 and 95." 

297 

298 # find value 

299 p = [2.787860e-9, -1.398845e-6, 3.287156e-4, -5.779136e-2, 5.038813, 1.402385e3] 

300 c = np.polyval(p, temp) 

301 return c 

302 

303 

304def water_density(temp): 

305 """ 

306 WATERDENSITY Calculate density of air - saturated water with temperature. 

307 

308 DESCRIPTION: 

309 waterDensity calculates the density of air - saturated water at a given % temperature using the 4 th order polynomial given by Jones(1992). 

310 

311 USAGE: 

312 density = waterDensity(T) 

313 

314 INPUTS: 

315 T - water temperature in the range 5 to 40[degC] 

316 

317 OUTPUTS: 

318 density - density of water[kg / m ^ 3] 

319 

320 ABOUT: 

321 author - Bradley E.Treeby 

322 date - 22 nd February 2018 

323 last update - 4 th April 2019 

324 

325 REFERENCES: 

326 [1] F.E.Jones and G.L.Harris(1992) "ITS-90 Density of Water 

327 Formulation for Volumetric Standards Calibration, " J. Res. Natl. 

328 Inst.Stand.Technol., 97(3), 335 - 340. 

329 """ 

330 # check limits 

331 assert 5 <= temp <= 40, "T must be between 5 and 40." 

332 

333 # calculate density of air - saturated water 

334 density = 999.84847 + 6.337563e-2 * temp - 8.523829e-3 * temp ** 2 + 6.943248e-5 * temp ** 3 - 3.821216e-7 * temp ** 4 

335 return density 

336 

337 

338def water_non_linearity(temp): 

339 """ 

340 WATERNONLINEARITY Calculate B/A of water with temperature. 

341 

342 DESCRIPTION: 

343 waterNonlinearity calculates the parameter of nonlinearity B/A at a 

344 given temperature using a fourth-order polynomial fitted to the data 

345 given by Beyer (1960). 

346 

347 USAGE: 

348 BonA = waterNonlinearity(T) 

349 

350 INPUTS: 

351 T - water temperature in the range 0 to 100 [degC] 

352 

353 OUTPUTS: 

354 BonA - parameter of nonlinearity 

355 

356 ABOUT: 

357 author - Bradley E. Treeby 

358 date - 22nd February 2018 

359 last update - 4th April 2019 

360 

361 REFERENCES: 

362 [1] R. T Beyer (1960) "Parameter of nonlinearity in fluids," J. 

363 Acoust. Soc. Am., 32(6), 719-721. 

364 

365 """ 

366 

367 # check limits 

368 assert 0 <= temp <= 100, "Temp must be between 0 and 100." 

369 

370 # find value 

371 p = [-4.587913769504693e-08, 1.047843302423604e-05, -9.355518377254833e-04, 5.380874771364909e-2, 4.186533937275504] 

372 BonA = np.polyval(p, temp); 

373 return BonA 

374 

375 

376def makeBall(Nx, Ny, Nz, cx, cy, cz, radius, plot_ball=False, binary=False): 

377 """ 

378 MAKEBALL Create a binary map of a filled ball within a 3D grid. 

379 

380 DESCRIPTION: 

381 makeBall creates a binary map of a filled ball within a 

382 three-dimensional grid (the ball position is denoted by 1's in the 

383 matrix with 0's elsewhere). A single grid point is taken as the ball 

384 centre thus the total diameter of the ball will always be an odd 

385 number of grid points. 

386 Args: 

387 Nx: size of the 3D grid in x-dimension [grid points] 

388 Ny: size of the 3D grid in y-dimension [grid points] 

389 Nz: size of the 3D grid in z-dimension [grid points] 

390 cx: centre of the ball in x-dimension [grid points] 

391 cy: centre of the ball in y-dimension [grid points] 

392 cz: centre of the ball in z-dimension [grid points] 

393 radius: ball radius [grid points] 

394 plot_ball: Boolean controlling whether the ball is plotted using voxelPlot (default = false) 

395 binary: Boolean controlling whether the ball map is returned as a double precision matrix (false) 

396 or a logical matrix (true) (default = false) 

397 

398 Returns: 

399 3D binary map of a filled ball 

400 """ 

401 # define literals 

402 MAGNITUDE = 1 

403 

404 # force integer values 

405 Nx = int(round(Nx)) 

406 Ny = int(round(Ny)) 

407 Nz = int(round(Nz)) 

408 cx = int(round(cx)) 

409 cy = int(round(cy)) 

410 cz = int(round(cz)) 

411 

412 # check for zero values 

413 if cx == 0: 

414 cx = int(floor(Nx / 2)) + 1 

415 

416 if cy == 0: 

417 cy = int(floor(Ny / 2)) + 1 

418 

419 if cz == 0: 

420 cz = int(floor(Nz / 2)) + 1 

421 

422 # create empty matrix 

423 ball = np.zeros((Nx, Ny, Nz)).astype(np.bool if binary else np.float32) 

424 

425 # define np.pixel map 

426 r = makePixelMap(Nx, Ny, Nz, 'Shift', [0, 0, 0]) 

427 

428 # create ball 

429 ball[r <= radius] = MAGNITUDE 

430 

431 # shift centre 

432 cx = cx - int(math.ceil(Nx / 2)) 

433 cy = cy - int(math.ceil(Ny / 2)) 

434 cz = cz - int(math.ceil(Nz / 2)) 

435 ball = np.roll(ball, (cx, cy, cz), axis=(0, 1, 2)) 

436 

437 # plot results 

438 if plot_ball: 

439 raise NotImplementedError 

440 # voxelPlot(double(ball)) 

441 return ball 

442 

443 

444def make_cart_sphere(radius, num_points, center_pos=(0, 0, 0), plot_sphere=False): 

445 """ 

446 make_cart_sphere creates a set of points in cartesian coordinates defining a sphere. 

447 

448 Args: 

449 radius: 

450 num_points: 

451 center_pos: 

452 plot_sphere: 

453 

454 Returns: 

455 

456 """ 

457 cx, cy, cz = center_pos 

458 

459 # generate angle functions using the Golden Section Spiral method 

460 inc = np.pi * (3 - np.sqrt(5)) 

461 off = 2 / num_points 

462 k = np.arange(0, num_points) 

463 y = k * off - 1 + (off / 2) 

464 r = np.sqrt(1 - (y ** 2)) 

465 phi = k * inc 

466 

467 # create the sphere 

468 sphere = radius * np.concatenate([np.cos(phi) * r[np.newaxis, :], y[np.newaxis, :], np.sin(phi) * r[np.newaxis, :]]) 

469 

470 # offset if needed 

471 sphere[0, :] = sphere[0, :] + cx 

472 sphere[1, :] = sphere[1, :] + cy 

473 sphere[2, :] = sphere[2, :] + cz 

474 

475 # plot results 

476 if plot_sphere: 

477 # select suitable axis scaling factor 

478 [x_sc, scale, prefix, _] = scale_SI(np.max(sphere)) 

479 

480 # create the figure 

481 plt.figure 

482 plt.style.use('seaborn-poster') 

483 ax = plt.axes(projection='3d') 

484 ax.plot3D(sphere[0, :] * scale, sphere[1, :] * scale, sphere[2, :] * scale, '.') 

485 ax.set_xlabel(f"[{prefix} m]") 

486 ax.set_ylabel(f"[{prefix} m]") 

487 ax.set_zlabel(f"[{prefix} m]") 

488 ax.axis('auto') 

489 ax.grid() 

490 plt.show() 

491 

492 

493def make_cart_circle(radius, num_points, center_pos=(0, 0), arc_angle=2 * np.pi, plot_circle=False): 

494 """ 

495 make_cart_circle creates a set of points in cartesian coordinates defining a circle or arc. 

496 

497 Args: 

498 radius: 

499 num_points: 

500 center_pos: 

501 arc_angle: Arc angle in radians. 

502 plot_circle: 

503 

504 Returns: 

505 2 x num_points array of cartesian coordinates 

506 

507 """ 

508 

509 # check for arc_angle input 

510 if arc_angle == 2 * np.pi: 

511 full_circle = True 

512 

513 cx = center_pos[0] 

514 cy = center_pos[1] 

515 

516 # create angles 

517 angles = np.arange(0, num_points) * arc_angle / num_points + np.pi / 2 

518 

519 # create cartesian grid 

520 circle = np.concatenate([radius * np.cos(angles[np.newaxis, :]), radius * np.sin(-angles[np.newaxis])]) 

521 

522 # offset if needed 

523 circle[0, :] = circle[0, :] + cx 

524 circle[1, :] = circle[1, :] + cy 

525 

526 # plot results 

527 if plot_circle: 

528 # select suitable axis scaling factor 

529 [_, scale, prefix, _] = scale_SI(np.max(abs(circle))) 

530 

531 # create the figure 

532 plt.figure() 

533 plt.plot(circle[1, :] * scale, circle[0, :] * scale, 'b.') 

534 plt.xlabel([f"y-position [{prefix} m]"]) 

535 plt.ylabel([f"x-position [{prefix} m]"]) 

536 plt.axis('equal') 

537 plt.show() 

538 

539 

540def makeDisc(Nx, Ny, cx, cy, radius, plot_disc=False): 

541 """ 

542 Create a binary map of a filled disc within a 2D grid. 

543 

544 makeDisc creates a binary map of a filled disc within a 

545 two-dimensional grid (the disc position is denoted by 1's in the 

546 matrix with 0's elsewhere). A single grid point is taken as the disc 

547 centre thus the total diameter of the disc will always be an odd 

548 number of grid points. As the returned disc has a constant radius, if 

549 used within a k-Wave grid where dx ~= dy, the disc will appear oval 

550 shaped. If part of the disc overlaps the grid edge, the rest of the 

551 disc will wrap to the grid edge on the opposite side. 

552 Args: 

553 Nx: 

554 Ny: 

555 cx: 

556 cy: 

557 radius: 

558 plot_disc: 

559 

560 Returns: 

561 

562 """ 

563 # define literals 

564 MAGNITUDE = 1 

565 

566 # force integer values 

567 Nx = int(round(Nx)) 

568 Ny = int(round(Ny)) 

569 cx = int(round(cx)) 

570 cy = int(round(cy)) 

571 

572 # check for zero values 

573 if cx == 0: 

574 cx = int(floor(Nx / 2)) + 1 

575 

576 if cy == 0: 

577 cy = int(floor(Ny / 2)) + 1 

578 

579 # check the inputs 

580 assert (0 <= cx < Nx) and (0 <= cy < Ny), 'Disc center must be within grid.' 

581 

582 # create empty matrix 

583 disc = np.zeros((Nx, Ny)) 

584 

585 # define np.pixel map 

586 r = makePixelMap(Nx, Ny, None, 'Shift', [0, 0]) 

587 

588 # create disc 

589 disc[r <= radius] = MAGNITUDE 

590 

591 # shift centre 

592 cx = cx - int(math.ceil(Nx / 2)) 

593 cy = cy - int(math.ceil(Ny / 2)) 

594 disc = np.roll(disc, (cx, cy), axis=(0, 1)) 

595 

596 # create the figure 

597 if plot_disc: 

598 raise NotImplementedError 

599 return disc 

600 

601 

602def makeCircle(Nx, Ny, cx, cy, radius, arc_angle=None, plot_circle=False): 

603 """ 

604 Create a binary map of a circle within a 2D grid. 

605 

606 makeCircle creates a binary map of a circle or arc (using the 

607 midpoint circle algorithm) within a two-dimensional grid (the circle 

608 position is denoted by 1's in the matrix with 0's elsewhere). A 

609 single grid point is taken as the circle centre thus the total 

610 diameter will always be an odd number of grid points. 

611 

612 Note: The centre of the circle and the radius are not constrained by 

613 the grid dimensions, so it is possible to create sections of circles, 

614 or a blank image if none of the circle intersects the grid. 

615 Args: 

616 Nx: 

617 Ny: 

618 cx: 

619 cy: 

620 radius: 

621 plot_disc: 

622 

623 Returns: 

624 

625 """ 

626 # define literals 

627 MAGNITUDE = 1 

628 

629 if arc_angle is None: 

630 arc_angle = 2 * np.pi 

631 elif arc_angle > 2 * np.pi: 

632 arc_angle = 2 * np.pi 

633 elif arc_angle < 0: 

634 arc_angle = 0 

635 

636 # force integer values 

637 Nx = int(round(Nx)) 

638 Ny = int(round(Ny)) 

639 cx = int(round(cx)) 

640 cy = int(round(cy)) 

641 radius = int(round(radius)) 

642 

643 # check for zero values 

644 if cx == 0: 

645 cx = int(floor(Nx / 2)) + 1 

646 

647 if cy == 0: 

648 cy = int(floor(Ny / 2)) + 1 

649 

650 # create empty matrix 

651 circle = np.zeros((Nx, Ny), dtype=int) 

652 

653 # initialise loop variables 

654 x = 0 

655 y = radius 

656 d = 1 - radius 

657 

658 if (cx >= 1) and (cx <= Nx) and ((cy - y) >= 1) and ((cy - y) <= Ny): 

659 circle[cx - 1, cy - y - 1] = MAGNITUDE 

660 

661 # draw the remaining cardinal points 

662 px = [cx, cx + y, cx - y] 

663 py = [cy + y, cy, cy] 

664 for point_index, (px_i, py_i) in enumerate(zip(px, py)): 

665 # check whether the point is within the arc made by arc_angle, and lies 

666 # within the grid 

667 if (np.arctan2(px_i - cx, py_i - cy) + np.pi) <= arc_angle: 

668 if (px_i >= 1) and (px_i <= Nx) and (py_i >= 1) and ( 

669 py_i <= Ny): 

670 circle[px_i - 1, py_i - 1] = MAGNITUDE 

671 

672 # loop through the remaining points using the midpoint circle algorithm 

673 while x < (y - 1): 

674 

675 x = x + 1 

676 if d < 0: 

677 d = d + x + x + 1 

678 else: 

679 y = y - 1 

680 a = x - y + 1 

681 d = d + a + a 

682 

683 # setup point indices (break coding standard for readability) 

684 px = [x + cx, y + cx, y + cx, x + cx, -x + cx, -y + cx, -y + cx, -x + cx] 

685 py = [y + cy, x + cy, -x + cy, -y + cy, -y + cy, -x + cy, x + cy, y + cy] 

686 

687 # loop through each point 

688 for point_index, (px_i, py_i) in enumerate(zip(px, py)): 

689 

690 # check whether the point is within the arc made by arc_angle, and 

691 # lies within the grid 

692 if (np.arctan2(px_i - cx, py_i - cy) + np.pi) <= arc_angle: 

693 if (px_i >= 1) and (px_i <= Nx) and (py_i >= 1) and (py_i <= Ny): 

694 circle[px_i - 1, py_i - 1] = MAGNITUDE 

695 

696 if plot_circle: 

697 plt.imshow(circle, cmap='gray_r') 

698 plt.ylabel('x-position [grid points]') 

699 plt.xlabel('y-position [grid points]') 

700 plt.show() 

701 

702 return circle 

703 

704 

705def makeCartCircle(radius, num_points, center_pos=None, arc_angle=(2 * np.pi), plot_circle=False): 

706 """ 

707 Create a 2D Cartesian circle or arc. 

708 

709 MakeCartCircle creates a 2 x num_points array of the Cartesian 

710 coordinates of points evenly distributed over a circle or arc (if 

711 arc_angle is given). 

712 Args: 

713 

714 Returns: 

715 

716 """ 

717 full_circle = (arc_angle == 2 * np.pi) 

718 

719 if center_pos is None: 

720 cx = cy = 0 

721 else: 

722 cx, cy = center_pos 

723 

724 # ensure there is only a total of num_points including the endpoints when 

725 # arc_angle is not equal to 2*pi 

726 if not full_circle: 

727 num_points = num_points - 1 

728 

729 # create angles 

730 angles = np.arange(0, num_points + 1) * arc_angle / num_points + np.pi / 2 

731 

732 # discard repeated final point if arc_angle is equal to 2*pi 

733 if full_circle: 

734 angles = angles[0:- 1] 

735 

736 # create cartesian grid 

737 # circle = flipud([radius*cos(angles); radius*sin(-angles)]); # B.0.3 

738 circle = np.vstack([radius * np.cos(angles), radius * np.sin(-angles)]) # B.0.4 

739 

740 # offset if needed 

741 circle[0, :] = circle[0, :] + cx 

742 circle[1, :] = circle[1, :] + cy 

743 

744 if plot_circle: 

745 raise NotImplementedError 

746 

747 return circle 

748 

749 

750def makePixelMap(Nx, Ny, Nz=None, *args): 

751 """ 

752 MAKEPIXELMAP Create matrix of grid point distances from the centre point. 

753 

754 DESCRIPTION: 

755 makePixelMap generates a matrix populated with values of how far each 

756 pixel in a grid is from the centre (given in pixel coordinates). Both 

757 single and double pixel centres can be used by setting the optional 

758 input parameter 'OriginSize'. For grids where the dimension size and 

759 centre pixel size are not both odd or even, the optional input 

760 parameter 'Shift' can be used to control the location of the 

761 centerpoint. 

762 

763 examples for a 2D pixel map: 

764 

765 Single pixel origin size for odd and even (with 'Shift' = [1 1] and 

766 [0 0], respectively) grid sizes: 

767 

768 x x x x x x x x x x x 

769 x 0 x x x x x x 0 x x 

770 x x x x x 0 x x x x x 

771 x x x x x x x x 

772 

773 Double pixel origin size for even and odd (with 'Shift' = [1 1] and 

774 [0 0], respectively) grid sizes: 

775 

776 x x x x x x x x x x x x x x 

777 x 0 0 x x x x x x x 0 0 x x 

778 x 0 0 x x x 0 0 x x 0 0 x x 

779 x x x x x x 0 0 x x x x x x 

780 x x x x x x x x x x 

781 

782 By default a single pixel centre is used which is shifted towards 

783 the final row and column. 

784 Args: 

785 *args: 

786 

787 Returns: 

788 

789 """ 

790 # define defaults 

791 origin_size = 'single' 

792 shift_def = 1 

793 

794 # detect whether the inputs are for two or three dimensions 

795 if Nz is None: 

796 map_dimension = 2 

797 shift = [shift_def, shift_def] 

798 else: 

799 map_dimension = 3 

800 shift = [shift_def, shift_def, shift_def] 

801 

802 # replace with user defined values if provided 

803 if len(args) > 0: 

804 assert len(args) % 2 == 0, 'Optional inputs must be entered as param, value pairs.' 

805 for input_index in range(0, len(args), 2): 

806 if args[input_index] == 'Shift': 

807 shift = args[input_index + 1] 

808 elif args[input_index] == 'OriginSize': 

809 origin_size = args[input_index + 1] 

810 else: 

811 raise ValueError('Unknown optional input.') 

812 

813 # catch input errors 

814 assert origin_size in ['single', 'double'], 'Unknown setting for optional input Center.' 

815 

816 assert len( 

817 shift) == map_dimension, f'Optional input Shift must have {map_dimension} elements for {map_dimension} dimensional input parameters.' 

818 

819 if map_dimension == 2: 

820 # create the maps for each dimension 

821 nx = createPixelDim(Nx, origin_size, shift[0]) 

822 ny = createPixelDim(Ny, origin_size, shift[1]) 

823 

824 # create plaid grids 

825 r_x, r_y = np.meshgrid(nx, ny, indexing='ij') 

826 

827 # extract the pixel radius 

828 r = np.sqrt(r_x ** 2 + r_y ** 2) 

829 if map_dimension == 3: 

830 # create the maps for each dimension 

831 nx = createPixelDim(Nx, origin_size, shift[0]) 

832 ny = createPixelDim(Ny, origin_size, shift[1]) 

833 nz = createPixelDim(Nz, origin_size, shift[2]) 

834 

835 # create plaid grids 

836 r_x, r_y, r_z = np.meshgrid(nx, ny, nz, indexing='ij') 

837 

838 # extract the pixel radius 

839 r = np.sqrt(r_x ** 2 + r_y ** 2 + r_z ** 2) 

840 return r 

841 

842 

843def createPixelDim(Nx, origin_size, shift): 

844 # Nested function to create the pixel radius variable 

845 

846 # grid dimension has an even number of points 

847 if Nx % 2 == 0: 

848 

849 # pixel numbering has a single centre point 

850 if origin_size == 'single': 

851 

852 # centre point is shifted towards the final pixel 

853 if shift == 1: 

854 nx = np.arange(-Nx / 2, Nx / 2 - 1 + 1, 1) 

855 

856 # centre point is shifted towards the first pixel 

857 else: 

858 nx = np.arange(-Nx / 2 + 1, Nx / 2 + 1, 1) 

859 

860 # pixel numbering has a double centre point 

861 else: 

862 nx = np.hstack([np.arange(-Nx / 2 + 1, 0 + 1, 1), np.arange(0, -Nx / 2 - 1 + 1, 1)]) 

863 

864 # grid dimension has an odd number of points 

865 else: 

866 

867 # pixel numbering has a single centre point 

868 if origin_size == 'single': 

869 nx = np.arange(-(Nx - 1) / 2, (Nx - 1) / 2 + 1, 1) 

870 

871 # pixel numbering has a double centre point 

872 else: 

873 

874 # centre point is shifted towards the final pixel 

875 if shift == 1: 

876 nx = np.hstack([np.arange(-(Nx - 1) / 2, 0 + 1, 1), np.arange(0, (Nx - 1) / 2 - 1 + 1, 1)]) 

877 

878 # centre point is shifted towards the first pixel 

879 else: 

880 nx = np.hstack([np.arange(-(Nx - 1) / 2 + 1, 0 + 1, 1), np.arange(0, (Nx - 1) / 2 + 1, 1)]) 

881 return nx 

882 

883 

884def makeLine( 

885 Nx: int, 

886 Ny: int, 

887 startpoint: Tuple[int, int], 

888 endpoint: Optional[Tuple[int, int]] = None, 

889 angle: Optional[float] = None, 

890 length: Optional[int] = None 

891) -> np.ndarray: 

892 # ========================================================================= 

893 # INPUT CHECKING 

894 # ========================================================================= 

895 

896 startpoint = np.array(startpoint, dtype=int) 

897 if endpoint is not None: 

898 endpoint = np.array(endpoint, dtype=int) 

899 

900 if len(startpoint) != 2: 

901 raise ValueError('startpoint should be a two-element vector.') 

902 

903 if np.any(startpoint < 1) or startpoint[0] > Nx or startpoint[1] > Ny: 

904 ValueError('The starting point must lie within the grid, between [1 1] and [Nx Ny].') 

905 

906 # ========================================================================= 

907 # LINE BETWEEN TWO POINTS OR ANGLED LINE? 

908 # ========================================================================= 

909 

910 if endpoint is not None: 

911 linetype = 'AtoB' 

912 a, b = startpoint, endpoint 

913 

914 # Addition => Fix Matlab2Python indexing 

915 a -= 1 

916 b -= 1 

917 else: 

918 linetype = 'angled' 

919 angle, linelength = angle, length 

920 

921 # ========================================================================= 

922 # MORE INPUT CHECKING 

923 # ========================================================================= 

924 

925 if linetype == 'AtoB': 

926 

927 # a and b must be different points 

928 if np.all(a == b): 

929 raise ValueError('The first and last points cannot be the same.') 

930 

931 # end point must be a two-element row vector 

932 if len(b) != 2: 

933 raise ValueError('endpoint should be a two-element vector.') 

934 

935 # a and b must be within the grid 

936 xx = np.array([a[0], b[0]], dtype=int) 

937 yy = np.array([a[1], b[1]], dtype=int) 

938 if np.any(a < 0) or np.any(b < 0) or np.any(xx > Nx - 1) or np.any(yy > Ny - 1): 

939 raise ValueError('Both the start and end points must lie within the grid.') 

940 

941 if linetype == 'angled': 

942 

943 # angle must lie between -np.pi and np.pi 

944 angle = angle % (2 * np.pi) 

945 if angle > np.pi: 

946 angle = angle - (2 * np.pi) 

947 elif angle < -np.pi: 

948 angle = angle + (2 * np.pi) 

949 

950 # ========================================================================= 

951 # CALCULATE A LINE FROM A TO B 

952 # ========================================================================= 

953 

954 if linetype == 'AtoB': 

955 

956 # define an empty grid to hold the line 

957 line = np.zeros((Nx, Ny)) 

958 

959 # find the equation of the line 

960 m = (b[1] - a[1]) / (b[0] - a[0]) # gradient of the line 

961 c = a[1] - m * a[0] # where the line crosses the y axis 

962 

963 if abs(m) < 1: 

964 

965 # start at the end with the smallest value of x 

966 if a[0] < b[0]: 

967 x, y = a 

968 x_end = b[0] 

969 else: 

970 x, y = b 

971 x_end = a[0] 

972 

973 # fill in the first point 

974 line[x, y] = 1 

975 

976 while x < x_end: 

977 # next points to try are 

978 poss_x = [x, x, x + 1, x + 1, x + 1] 

979 poss_y = [y - 1, y + 1, y - 1, y, y + 1] 

980 

981 # find the point closest to the line 

982 true_y = m * poss_x + c 

983 diff = (poss_y - true_y) ** 2 

984 index = matlab_find(diff == min(diff))[0] 

985 

986 # the next point 

987 x = poss_x[index[0] - 1] 

988 y = poss_y[index[0] - 1] 

989 

990 # add the point to the line 

991 line[x - 1, y - 1] = 1 

992 

993 elif not np.isinf(abs(m)): 

994 

995 # start at the end with the smallest value of y 

996 if a[1] < b[1]: 

997 x = a[0] 

998 y = a[1] 

999 y_end = b[1] 

1000 else: 

1001 x = b[0] 

1002 y = b[1] 

1003 y_end = a[1] 

1004 

1005 # fill in the first point 

1006 line[x, y] = 1 

1007 

1008 while y < y_end: 

1009 # next points to try are 

1010 poss_y = [y, y, y + 1, y + 1, y + 1] 

1011 poss_x = [x - 1, x + 1, x - 1, x, x + 1] 

1012 

1013 # find the point closest to the line 

1014 true_x = (poss_y - c) / m 

1015 diff = (poss_x - true_x) ** 2 

1016 index = matlab_find(diff == min(diff))[0] 

1017 

1018 # the next point 

1019 x = poss_x[index[0] - 1] 

1020 y = poss_y[index[0] - 1] 

1021 

1022 # add the point to the line 

1023 line[x, y] = 1 

1024 

1025 else: # m = +-Inf 

1026 

1027 # start at the end with the smallest value of y 

1028 if a[1] < b[1]: 

1029 x = a[0] 

1030 y = a[1] 

1031 y_end = b[1] 

1032 else: 

1033 x = b[0] 

1034 y = b[1] 

1035 y_end = a[1] 

1036 

1037 # fill in the first point 

1038 line[x, y] = 1 

1039 

1040 while y < y_end: 

1041 # next point 

1042 y = y + 1 

1043 

1044 # add the point to the line 

1045 line[x, y] = 1 

1046 

1047 # ========================================================================= 

1048 # CALCULATE AN ANGLED LINE 

1049 # ========================================================================= 

1050 

1051 elif linetype == 'angled': 

1052 

1053 # define an empty grid to hold the line 

1054 line = np.zeros((Nx, Ny)) 

1055 

1056 # start at the atart 

1057 x, y = startpoint 

1058 

1059 # fill in the first point 

1060 line[x - 1, y - 1] = 1 

1061 

1062 # initialise the current length of the line 

1063 line_length = 0 

1064 

1065 if abs(angle) == np.pi: 

1066 

1067 while line_length < linelength: 

1068 

1069 # next point 

1070 y = y + 1 

1071 

1072 # stop the points incrementing at the edges 

1073 if y > Ny: 

1074 break 

1075 

1076 # add the point to the line 

1077 line[x - 1, y - 1] = 1 

1078 

1079 # calculate the current length of the line 

1080 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1081 

1082 elif (angle < np.pi) and (angle > np.pi / 2): 

1083 

1084 # define the equation of the line 

1085 m = -np.tan(angle - np.pi / 2) # gradient of the line 

1086 c = y - m * x # where the line crosses the y axis 

1087 

1088 while line_length < linelength: 

1089 

1090 # next points to try are 

1091 poss_x = np.array([x - 1, x - 1, x]) 

1092 poss_y = np.array([y, y + 1, y + 1]) 

1093 

1094 # find the point closest to the line 

1095 true_y = m * poss_x + c 

1096 diff = (poss_y - true_y) ** 2 

1097 index = matlab_find(diff == min(diff))[0] 

1098 

1099 # the next point 

1100 x = poss_x[index[0] - 1] 

1101 y = poss_y[index[0] - 1] 

1102 

1103 # stop the points incrementing at the edges 

1104 if (x < 0) or (y > Ny - 1): 

1105 break 

1106 

1107 # add the point to the line 

1108 line[x - 1, y - 1] = 1 

1109 

1110 # calculate the current length of the line 

1111 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1112 

1113 elif angle == np.pi / 2: 

1114 

1115 while line_length < linelength: 

1116 

1117 # next point 

1118 x = x - 1 

1119 

1120 # stop the points incrementing at the edges 

1121 if x < 1: 

1122 break 

1123 

1124 # add the point to the line 

1125 line[x - 1, y - 1] = 1 

1126 

1127 # calculate the current length of the line 

1128 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1129 

1130 elif (angle < np.pi / 2) and (angle > 0): 

1131 

1132 # define the equation of the line 

1133 m = np.tan(np.pi / 2 - angle) # gradient of the line 

1134 c = y - m * x # where the line crosses the y axis 

1135 

1136 while line_length < linelength: 

1137 

1138 # next points to try are 

1139 poss_x = np.array([x - 1, x - 1, x]) 

1140 poss_y = np.array([y, y - 1, y - 1]) 

1141 

1142 # find the point closest to the line 

1143 true_y = m * poss_x + c 

1144 diff = (poss_y - true_y) ** 2 

1145 index = matlab_find(diff == min(diff))[0] 

1146 

1147 # the next point 

1148 x = poss_x[index[0] - 1] 

1149 y = poss_y[index[0] - 1] 

1150 

1151 # stop the points incrementing at the edges 

1152 if (x < 1) or (y < 1): 

1153 break 

1154 

1155 # add the point to the line 

1156 line[x - 1, y - 1] = 1 

1157 

1158 # calculate the current length of the line 

1159 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1160 

1161 elif angle == 0: 

1162 

1163 while line_length < linelength: 

1164 

1165 # next point 

1166 y = y - 1 

1167 

1168 # stop the points incrementing at the edges 

1169 if y < 1: 

1170 break 

1171 

1172 # add the point to the line 

1173 line[x - 1, y - 1] = 1 

1174 

1175 # calculate the current length of the line 

1176 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1177 

1178 elif (angle < 0) and (angle > -np.pi / 2): 

1179 

1180 # define the equation of the line 

1181 m = -np.tan(np.pi / 2 + angle) # gradient of the line 

1182 c = y - m * x # where the line crosses the y axis 

1183 

1184 while line_length < linelength: 

1185 

1186 # next points to try are 

1187 poss_x = np.array([x + 1, x + 1, x]) 

1188 poss_y = np.array([y, y - 1, y - 1]) 

1189 

1190 # find the point closest to the line 

1191 true_y = m * poss_x + c 

1192 diff = (poss_y - true_y) ** 2 

1193 index = matlab_find(diff == min(diff))[0] 

1194 

1195 # the next point 

1196 x = poss_x[index[0] - 1] 

1197 y = poss_y[index[0] - 1] 

1198 

1199 # stop the points incrementing at the edges 

1200 if (x > Nx) or (y < 1): 

1201 break 

1202 

1203 # add the point to the line 

1204 line[x - 1, y - 1] = 1 

1205 

1206 # calculate the current length of the line 

1207 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1208 

1209 elif angle == -np.pi / 2: 

1210 

1211 while line_length < linelength: 

1212 

1213 # next point 

1214 x = x + 1 

1215 

1216 # stop the points incrementing at the edges 

1217 if x > Nx: 

1218 break 

1219 

1220 # add the point to the line 

1221 line[x - 1, y - 1] = 1 

1222 

1223 # calculate the current length of the line 

1224 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1225 

1226 elif (angle < -np.pi / 2) and (angle > -np.pi): 

1227 

1228 # define the equation of the line 

1229 m = np.tan(-angle - np.pi / 2) # gradient of the line 

1230 c = y - m * x # where the line crosses the y axis 

1231 

1232 while line_length < linelength: 

1233 

1234 # next points to try are 

1235 poss_x = np.array([x + 1, x + 1, x]) 

1236 poss_y = np.array([y, y + 1, y + 1]) 

1237 

1238 # find the point closest to the line 

1239 true_y = m * poss_x + c 

1240 diff = (poss_y - true_y) ** 2 

1241 index = matlab_find(diff == min(diff))[0] 

1242 

1243 # the next point 

1244 x = poss_x[index[0] - 1] 

1245 y = poss_y[index[0] - 1] 

1246 

1247 # stop the points incrementing at the edges 

1248 if (x > Nx) or (y > Ny): 

1249 break 

1250 

1251 # add the point to the line 

1252 line[x - 1, y - 1] = 1 

1253 

1254 # calculate the current length of the line 

1255 line_length = np.sqrt((x - startpoint[0]) ** 2 + (y - startpoint[1]) ** 2) 

1256 

1257 return line 

1258 

1259 

1260def makeArc(grid_size: np.ndarray, arc_pos: np.ndarray, radius, diameter, focus_pos: np.ndarray): 

1261 # force integer input values 

1262 grid_size = grid_size.round().astype(int) 

1263 arc_pos = arc_pos.round().astype(int) 

1264 diameter = int(round(diameter)) 

1265 focus_pos = focus_pos.round().astype(int) 

1266 

1267 try: 

1268 radius = int(radius) 

1269 except OverflowError: 

1270 radius = float(radius) 

1271 

1272 # check the input ranges 

1273 if np.any(grid_size < 1): 

1274 raise ValueError('The grid size must be positive.') 

1275 if radius <= 0: 

1276 raise ValueError('The radius must be positive.') 

1277 

1278 if diameter <= 0: 

1279 raise ValueError('The diameter must be positive.') 

1280 

1281 if np.any(arc_pos < 1) or np.any(arc_pos > grid_size): 

1282 raise ValueError('The centre of the arc must be within the grid.') 

1283 

1284 if diameter > 2 * radius: 

1285 raise ValueError('The diameter of the arc must be less than twice the radius of curvature.') 

1286 

1287 if diameter % 2 != 1: 

1288 raise ValueError('The diameter must be an odd number of grid points.') 

1289 

1290 if np.all(arc_pos == focus_pos): 

1291 raise ValueError('The focus_pos must be different to the arc_pos.') 

1292 

1293 # assign variable names to vector components 

1294 Nx, Ny = grid_size 

1295 ax, ay = arc_pos 

1296 fx, fy = focus_pos 

1297 

1298 # ========================================================================= 

1299 # CREATE ARC 

1300 # ========================================================================= 

1301 

1302 if not np.isinf(radius): 

1303 

1304 # find half the arc angle 

1305 half_arc_angle = np.arcsin(diameter / 2 / radius) 

1306 

1307 # find centre of circle on which the arc lies 

1308 distance_cf = np.sqrt((ax - fx) ** 2 + (ay - fy) ** 2) 

1309 cx = round(radius / distance_cf * (fx - ax) + ax) 

1310 cy = round(radius / distance_cf * (fy - ay) + ay) 

1311 c = np.array([cx, cy]) 

1312 

1313 # create circle 

1314 arc = makeCircle(Nx, Ny, cx, cy, radius) 

1315 

1316 # form vector from the geometric arc centre to the arc midpoint 

1317 v1 = arc_pos - c 

1318 

1319 # calculate length of vector 

1320 l1 = np.sqrt(sum((arc_pos - c) ** 2)) 

1321 

1322 # extract all points that form part of the arc 

1323 arc_ind = matlab_find(arc, mode='eq', val=1) 

1324 

1325 # loop through the arc points 

1326 for arc_ind_i in arc_ind: 

1327 

1328 # extract the indices of the current point 

1329 x_ind, y_ind = ind2sub([Nx, Ny], arc_ind_i) 

1330 p = np.array([x_ind, y_ind]) 

1331 

1332 # form vector from the geometric arc centre to the current point 

1333 v2 = p - c 

1334 

1335 # calculate length of vector 

1336 l2 = np.sqrt(sum((p - c) ** 2)) 

1337 

1338 # find the angle between the two vectors using the dot product, 

1339 # normalised using the vector lengths 

1340 theta = np.arccos(sum(v1 * v2 / (l1 * l2))) 

1341 

1342 # if the angle is greater than the half angle of the arc, remove 

1343 # it from the arc 

1344 if theta > half_arc_angle: 

1345 arc = matlab_assign(arc, arc_ind_i - 1, 0) 

1346 else: 

1347 

1348 # calculate arc direction angle, then rotate by 90 degrees 

1349 ang = np.arctan((fx - ax) / (fy - ay)) + np.pi / 2 

1350 

1351 # draw lines to create arc with infinite radius 

1352 arc = np.logical_or( 

1353 makeLine(Nx, Ny, arc_pos, endpoint=None, angle=ang, length=(diameter - 1) // 2), 

1354 makeLine(Nx, Ny, arc_pos, endpoint=None, angle=(ang + np.pi), length=(diameter - 1) // 2) 

1355 ) 

1356 return arc 

1357 

1358 

1359def ind2sub(array_shape, ind): 

1360 # Matlab style ind2sub 

1361 # row, col = np.unravel_index(ind - 1, array_shape, order='F') 

1362 # return np.squeeze(row) + 1, np.squeeze(col) + 1 

1363 

1364 indices = np.unravel_index(ind - 1, array_shape, order='F') 

1365 indices = (np.squeeze(index) + 1 for index in indices) 

1366 return indices 

1367 

1368 

1369def sub2ind(array_shape, x, y, z) -> np.ndarray: 

1370 results = [] 

1371 x, y, z = np.squeeze(x), np.squeeze(y), np.squeeze(z) 

1372 for x_i, y_i, z_i in zip(x, y, z): 

1373 index = np.ravel_multi_index((x_i, y_i, z_i), dims=array_shape, order='F') 

1374 results.append(index) 

1375 return np.array(results) 

1376 

1377 

1378def makePixelMapPoint(grid_size, centre_pos) -> np.ndarray: 

1379 # check for number of dimensions 

1380 num_dim = len(grid_size) 

1381 

1382 # check that centre_pos has the same dimensions 

1383 if len(grid_size) != len(centre_pos): 

1384 raise ValueError('The inputs centre_pos and grid_size must have the same number of dimensions.') 

1385 

1386 if num_dim == 2: 

1387 # assign inputs and force to be integers 

1388 Nx, Ny = grid_size.astype(int) 

1389 cx, cy = centre_pos.astype(int) 

1390 

1391 # generate index vectors in each dimension 

1392 nx = np.arange(0, Nx) - cx + 1 

1393 ny = np.arange(0, Ny) - cy + 1 

1394 

1395 # combine index matrices 

1396 pixel_map = np.zeros((Nx, Ny)) 

1397 pixel_map += (nx ** 2)[:, None] 

1398 pixel_map += (ny ** 2)[None, :] 

1399 pixel_map = np.sqrt(pixel_map) 

1400 

1401 elif num_dim == 3: 

1402 

1403 # assign inputs and force to be integers 

1404 Nx, Ny, Nz = grid_size.astype(int) 

1405 cx, cy, cz = centre_pos.astype(int) 

1406 

1407 # generate index vectors in each dimension 

1408 nx = np.arange(0, Nx) - cx + 1 

1409 ny = np.arange(0, Ny) - cy + 1 

1410 nz = np.arange(0, Nz) - cz + 1 

1411 

1412 # combine index matrices 

1413 pixel_map = np.zeros((Nx, Ny, Nz)) 

1414 pixel_map += (nx ** 2)[:, None, None] 

1415 pixel_map += (ny ** 2)[None, :, None] 

1416 pixel_map += (nz ** 2)[None, None, :] 

1417 pixel_map = np.sqrt(pixel_map) 

1418 

1419 else: 

1420 # throw error 

1421 raise ValueError('Grid size must be 2 or 3D.') 

1422 

1423 return pixel_map 

1424 

1425 

1426def makePixelMapPlane(grid_size, normal, point): 

1427 # error checking 

1428 if np.all(normal == 0): 

1429 raise ValueError('Normal vector should not be zero.') 

1430 

1431 # check for number of dimensions 

1432 num_dim = len(grid_size) 

1433 

1434 if num_dim == 2: 

1435 # assign inputs and force to be integers 

1436 Nx = round(grid_size[0]) 

1437 Ny = round(grid_size[1]) 

1438 

1439 # create coordinate meshes 

1440 [px, py] = np.meshgrid(Nx, Ny) 

1441 [pointx, pointy] = np.meshgrid(np.ones((1, Nx)) * point[0], np.ones(1, Ny) * point[1]) 

1442 [nx, ny] = np.meshgrid(np.ones((1, Nx)) * normal[0], np.ones(1, Ny) * normal[2]) 

1443 

1444 # calculate distance according to Eq. (6) at 

1445 # http://mathworld.wolfram.com/Point-PlaneDistance.html 

1446 pixel_map = np.abs((px - pointx) * nx + (py - pointy) * ny) / np.sqrt(sum(normal ** 2)) 

1447 

1448 elif num_dim == 3: 

1449 

1450 # assign inputs and force to be integers 

1451 Nx = np.round(grid_size[0]) 

1452 Ny = np.round(grid_size[1]) 

1453 Nz = np.round(grid_size[2]) 

1454 

1455 # create coordinate meshes 

1456 px, py, pz = np.meshgrid(np.arange(1, Nx + 1), np.arange(1, Ny + 1), np.arange(1, Nz + 1), indexing='ij') 

1457 pointx, pointy, pointz = np.meshgrid(np.ones(Nx) * point[0], np.ones(Ny) * point[1], np.ones(Nz) * point[2], 

1458 indexing='ij') 

1459 nx, ny, nz = np.meshgrid(np.ones(Nx) * normal[0], np.ones(Ny) * normal[1], np.ones(Nz) * normal[2], 

1460 indexing='ij') 

1461 

1462 # calculate distance according to Eq. (6) at 

1463 # http://mathworld.wolfram.com/Point-PlaneDistance.html 

1464 pixel_map = np.abs((px - pointx) * nx + (py - pointy) * ny + (pz - pointz) * nz) / np.sqrt(sum(normal ** 2)) 

1465 

1466 else: 

1467 # throw error 

1468 raise ValueError('Grid size must be 2 or 3D.') 

1469 

1470 return pixel_map 

1471 

1472 

1473def makeBowl(grid_size, bowl_pos, radius, diameter, focus_pos, binary=False, remove_overlap=False): 

1474 # ========================================================================= 

1475 # DEFINE LITERALS 

1476 # ========================================================================= 

1477 

1478 # threshold used to find the closest point to the radius 

1479 THRESHOLD = 0.5 

1480 

1481 # number of grid points to expand the bounding box compared to 

1482 # sqrt(2)*diameter 

1483 BOUNDING_BOX_EXP = 2 

1484 

1485 # ========================================================================= 

1486 # INPUT CHECKING 

1487 # ========================================================================= 

1488 

1489 # force integer input values 

1490 grid_size = np.round(grid_size).astype(int) 

1491 bowl_pos = np.round(bowl_pos).astype(int) 

1492 focus_pos = np.round(focus_pos).astype(int) 

1493 diameter = np.round(diameter) 

1494 radius = np.round(radius) 

1495 

1496 # check the input ranges 

1497 if np.any(grid_size < 1): 

1498 raise ValueError('The grid size must be positive.') 

1499 if np.any(bowl_pos < 1) or np.any(bowl_pos > grid_size): 

1500 raise ValueError('The centre of the bowl must be within the grid.') 

1501 if radius <= 0: 

1502 raise ValueError('The radius must be positive.') 

1503 if diameter <= 0: 

1504 raise ValueError('The diameter must be positive.') 

1505 if diameter > (2 * radius): 

1506 raise ValueError('The diameter of the bowl must be less than twice the radius of curvature.') 

1507 if diameter % 2 == 0: 

1508 raise ValueError('The diameter must be an odd number of grid points.') 

1509 if np.all(bowl_pos == focus_pos): 

1510 raise ValueError('The focus_pos must be different to the bowl_pos.') 

1511 

1512 # ========================================================================= 

1513 # BOUND THE GRID TO SPEED UP CALCULATION 

1514 # ========================================================================= 

1515 

1516 # create bounding box slightly larger than bowl diameter * sqrt(2) 

1517 Nx = np.round(np.sqrt(2) * diameter).astype(int) + BOUNDING_BOX_EXP 

1518 Ny = Nx 

1519 Nz = Nx 

1520 grid_size_sm = np.array([Nx, Ny, Nz]) 

1521 

1522 # set the bowl position to be the centre of the bounding box 

1523 bx = np.ceil(Nx / 2).astype(int) 

1524 by = np.ceil(Ny / 2).astype(int) 

1525 bz = np.ceil(Nz / 2).astype(int) 

1526 bowl_pos_sm = np.array([bx, by, bz]) 

1527 

1528 # set the focus position to be in the direction specified by the user 

1529 fx = bx + (focus_pos[0] - bowl_pos[0]) 

1530 fy = by + (focus_pos[1] - bowl_pos[1]) 

1531 fz = bz + (focus_pos[2] - bowl_pos[2]) 

1532 focus_pos_sm = [fx, fy, fz] 

1533 

1534 # preallocate storage variable 

1535 if binary: 

1536 bowl_sm = np.zeros((Nx, Ny, Nz), dtype=bool) 

1537 else: 

1538 bowl_sm = np.zeros((Nx, Ny, Nz)) 

1539 

1540 # ========================================================================= 

1541 # CREATE DISTANCE MATRIX 

1542 # ========================================================================= 

1543 

1544 if not np.isinf(radius): 

1545 

1546 # find half the arc angle 

1547 half_arc_angle = np.arcsin(diameter / (2 * radius)) 

1548 

1549 # find centre of sphere on which the bowl lies 

1550 distance_cf = np.sqrt((bx - fx) ** 2 + (by - fy) ** 2 + (bz - fz) ** 2) 

1551 cx = round(radius / distance_cf * (fx - bx) + bx) 

1552 cy = round(radius / distance_cf * (fy - by) + by) 

1553 cz = round(radius / distance_cf * (fz - bz) + bz) 

1554 c = np.array([cx, cy, cz]) 

1555 

1556 # generate matrix with distance from the centre 

1557 pixel_map = makePixelMapPoint(grid_size_sm, c) 

1558 

1559 # set search radius to bowl radius 

1560 search_radius = radius 

1561 

1562 else: 

1563 

1564 # generate matrix with distance from the centre 

1565 pixel_map = makePixelMapPlane(grid_size_sm, bowl_pos_sm - focus_pos_sm, bowl_pos_sm) 

1566 

1567 # set search radius to 0 (the disc is flat) 

1568 search_radius = 0 

1569 

1570 # calculate distance from search radius 

1571 pixel_map = np.abs(pixel_map - search_radius) 

1572 

1573 # ========================================================================= 

1574 # DIMENSION 1 

1575 # ========================================================================= 

1576 

1577 # find the grid point that corresponds to the outside of the bowl in the 

1578 # first dimension in both directions (the index gives the distance along 

1579 # this dimension) 

1580 value_forw, index_forw = pixel_map.min(axis=0), pixel_map.argmin(axis=0) 

1581 value_back, index_back = np.flip(pixel_map, axis=0).min(axis=0), np.flip(pixel_map, axis=0).argmin(axis=0) 

1582 

1583 # extract the linear index in the y-z plane of the values that lie on the 

1584 # bowl surface 

1585 yz_ind_forw = matlab_find(value_forw < THRESHOLD) 

1586 yz_ind_back = matlab_find(value_back < THRESHOLD) 

1587 

1588 # use these subscripts to extract the x-index of the grid points that lie 

1589 # on the bowl surface 

1590 x_ind_forw = index_forw.flatten(order='F')[yz_ind_forw - 1] + 1 

1591 x_ind_back = index_back.flatten(order='F')[yz_ind_back - 1] + 1 

1592 

1593 # convert the linear index to equivalent subscript values 

1594 y_ind_forw, z_ind_forw = ind2sub([Ny, Nz], yz_ind_forw) 

1595 y_ind_back, z_ind_back = ind2sub([Ny, Nz], yz_ind_back) 

1596 

1597 # combine x-y-z indices into a linear index 

1598 linear_index_forw = sub2ind([Nx, Ny, Nz], x_ind_forw - 1, y_ind_forw - 1, z_ind_forw - 1) + 1 

1599 linear_index_back = sub2ind([Nx, Ny, Nz], Nx - x_ind_back, y_ind_back - 1, z_ind_back - 1) + 1 

1600 

1601 # assign these values to the bowl 

1602 bowl_sm = matlab_assign(bowl_sm, linear_index_forw - 1, 1) 

1603 bowl_sm = matlab_assign(bowl_sm, linear_index_back - 1, 1) 

1604 

1605 # set existing bowl values to a distance of zero in the pixel map (this 

1606 # avoids problems with overlapping pixels) 

1607 pixel_map[bowl_sm == 1] = 0 

1608 

1609 # ========================================================================= 

1610 # DIMENSION 2 

1611 # ========================================================================= 

1612 

1613 # find the grid point that corresponds to the outside of the bowl in the 

1614 # second dimension in both directions (the pixel map is first re-ordered to 

1615 # [X, Y, Z] -> [Y, Z, X]) 

1616 pixel_map_temp = np.transpose(pixel_map, (1, 2, 0)) 

1617 value_forw, index_forw = pixel_map_temp.min(axis=0), pixel_map_temp.argmin(axis=0) 

1618 value_back, index_back = np.flip(pixel_map_temp, axis=0).min(axis=0), np.flip(pixel_map_temp, axis=0).argmin(axis=0) 

1619 del pixel_map_temp 

1620 

1621 # extract the linear index in the y-z plane of the values that lie on the 

1622 # bowl surface 

1623 zx_ind_forw = matlab_find(value_forw < THRESHOLD) 

1624 zx_ind_back = matlab_find(value_back < THRESHOLD) 

1625 

1626 # use these subscripts to extract the y-index of the grid points that lie 

1627 # on the bowl surface 

1628 y_ind_forw = index_forw.flatten(order='F')[zx_ind_forw - 1] + 1 

1629 y_ind_back = index_back.flatten(order='F')[zx_ind_back - 1] + 1 

1630 

1631 # convert the linear index to equivalent subscript values 

1632 z_ind_forw, x_ind_forw = ind2sub([Nz, Nx], zx_ind_forw) 

1633 z_ind_back, x_ind_back = ind2sub([Nz, Nx], zx_ind_back) 

1634 

1635 # combine x-y-z indices into a linear index 

1636 linear_index_forw = sub2ind([Nx, Ny, Nz], x_ind_forw - 1, y_ind_forw - 1, z_ind_forw - 1) + 1 

1637 linear_index_back = sub2ind([Nx, Ny, Nz], x_ind_back - 1, Ny - y_ind_back, z_ind_back - 1) + 1 

1638 

1639 # assign these values to the bowl 

1640 bowl_sm = matlab_assign(bowl_sm, linear_index_forw - 1, 1) 

1641 bowl_sm = matlab_assign(bowl_sm, linear_index_back - 1, 1) 

1642 

1643 # set existing bowl values to a distance of zero in the pixel map (this 

1644 # avoids problems with overlapping pixels) 

1645 pixel_map[bowl_sm == 1] = 0 

1646 

1647 # ========================================================================= 

1648 # DIMENSION 3 

1649 # ========================================================================= 

1650 

1651 # find the grid point that corresponds to the outside of the bowl in the 

1652 # third dimension in both directions (the pixel map is first re-ordered to 

1653 # [X, Y, Z] -> [Z, X, Y]) 

1654 pixel_map_temp = np.transpose(pixel_map, (2, 0, 1)) 

1655 value_forw, index_forw = pixel_map_temp.min(axis=0), pixel_map_temp.argmin(axis=0) 

1656 value_back, index_back = np.flip(pixel_map_temp, axis=0).min(axis=0), np.flip(pixel_map_temp, axis=0).argmin(axis=0) 

1657 del pixel_map_temp 

1658 

1659 # extract the linear index in the y-z plane of the values that lie on the 

1660 # bowl surface 

1661 xy_ind_forw = matlab_find(value_forw < THRESHOLD) 

1662 xy_ind_back = matlab_find(value_back < THRESHOLD) 

1663 

1664 # use these subscripts to extract the z-index of the grid points that lie 

1665 # on the bowl surface 

1666 z_ind_forw = index_forw.flatten(order='F')[xy_ind_forw - 1] + 1 

1667 z_ind_back = index_back.flatten(order='F')[xy_ind_back - 1] + 1 

1668 

1669 # convert the linear index to equivalent subscript values 

1670 x_ind_forw, y_ind_forw = ind2sub([Nx, Ny], xy_ind_forw) 

1671 x_ind_back, y_ind_back = ind2sub([Nx, Ny], xy_ind_back) 

1672 

1673 # combine x-y-z indices into a linear index 

1674 linear_index_forw = sub2ind([Nx, Ny, Nz], x_ind_forw - 1, y_ind_forw - 1, z_ind_forw - 1) + 1 

1675 linear_index_back = sub2ind([Nx, Ny, Nz], x_ind_back - 1, y_ind_back - 1, Nz - z_ind_back) + 1 

1676 

1677 # assign these values to the bowl 

1678 bowl_sm = matlab_assign(bowl_sm, linear_index_forw - 1, 1) 

1679 bowl_sm = matlab_assign(bowl_sm, linear_index_back - 1, 1) 

1680 

1681 # ========================================================================= 

1682 # RESTRICT SPHERE TO BOWL 

1683 # ========================================================================= 

1684 

1685 # remove grid points within the sphere that do not form part of the bowl 

1686 if not np.isinf(radius): 

1687 

1688 # form vector from the geometric bowl centre to the back of the bowl 

1689 v1 = bowl_pos_sm - c 

1690 

1691 # calculate length of vector 

1692 l1 = np.sqrt(sum((bowl_pos_sm - c) ** 2)) 

1693 

1694 # loop through the non-zero elements in the bowl matrix 

1695 bowl_ind = matlab_find(bowl_sm == 1)[:, 0] 

1696 for bowl_ind_i in bowl_ind: 

1697 

1698 # extract the indices of the current point 

1699 x_ind, y_ind, z_ind = ind2sub([Nx, Ny, Nz], bowl_ind_i) 

1700 p = np.array([x_ind, y_ind, z_ind]) 

1701 

1702 # form vector from the geometric bowl centre to the current point 

1703 # on the bowl 

1704 v2 = p - c 

1705 

1706 # calculate length of vector 

1707 l2 = np.sqrt(sum((p - c) ** 2)) 

1708 

1709 # find the angle between the two vectors using the dot product, 

1710 # normalised using the vector lengths 

1711 theta = np.arccos(sum(v1 * v2 / (l1 * l2))) 

1712 

1713 # # alternative calculation normalised using radius of curvature 

1714 # theta2 = acos(sum( v1 .* v2 ./ radius**2 )) 

1715 

1716 # if the angle is greater than the half angle of the bowl, remove 

1717 # it from the bowl 

1718 if theta > half_arc_angle: 

1719 bowl_sm = matlab_assign(bowl_sm, bowl_ind_i - 1, 0) 

1720 

1721 else: 

1722 

1723 # form a distance map from the centre of the disc 

1724 pixelMapPoint = makePixelMapPoint(grid_size_sm, bowl_pos_sm) 

1725 

1726 # set all points in the disc greater than the diameter to zero 

1727 bowl_sm[pixelMapPoint > (diameter / 2)] = 0 

1728 

1729 # ========================================================================= 

1730 # REMOVE OVERLAPPED POINTS 

1731 # ========================================================================= 

1732 

1733 if remove_overlap: 

1734 

1735 # define the shapes that capture the overlapped points, along with the 

1736 # corresponding mask of which point to delete 

1737 overlap_shapes = [] 

1738 overlap_delete = [] 

1739 

1740 shape = np.zeros((3, 3, 3)) 

1741 shape[0, 0, :] = 1 

1742 shape[1, 1, :] = 1 

1743 shape[2, 2, :] = 1 

1744 shape[0, 1, 1] = 1 

1745 shape[1, 2, 1] = 1 

1746 overlap_shapes.append(shape) 

1747 

1748 delete = np.zeros((3, 3, 3)) 

1749 delete[0, 1, 1] = 1 

1750 delete[0, 2, 1] = 1 

1751 overlap_delete.append(delete) 

1752 

1753 shape = np.zeros((3, 3, 3)) 

1754 shape[0, 0, :] = 1 

1755 shape[1, 1, :] = 1 

1756 shape[2, 2, :] = 1 

1757 shape[0, 1, 1] = 1 

1758 overlap_shapes.append(shape) 

1759 

1760 delete = np.zeros((3, 3, 3)) 

1761 delete[0, 1, 1] = 1 

1762 overlap_delete.append(delete) 

1763 

1764 shape = np.zeros((3, 3, 3)) 

1765 shape[0:2, 0, :] = 1 

1766 shape[2, 1, :] = 1 

1767 shape[1, 1, 1] = 1 

1768 overlap_shapes.append(shape) 

1769 

1770 delete = np.zeros((3, 3, 3)) 

1771 delete[1, 1, 1] = 1 

1772 overlap_delete.append(delete) 

1773 

1774 shape = np.zeros((3, 3, 3)) 

1775 shape[0, 0, :] = 1 

1776 shape[1, 1, :] = 1 

1777 shape[2, 2, :] = 1 

1778 shape[0, 1, 0] = 1 

1779 overlap_shapes.append(shape) 

1780 

1781 delete = np.zeros((3, 3, 3)) 

1782 delete[0, 1, 0] = 1 

1783 overlap_delete.append(delete) 

1784 

1785 shape = np.zeros((3, 3, 3)) 

1786 shape[0:2, 1, :] = 1 

1787 shape[2, 2, :] = 1 

1788 shape[2, 1, 0] = 1 

1789 overlap_shapes.append(shape) 

1790 

1791 delete = np.zeros((3, 3, 3)) 

1792 delete[2, 1, 0] = 1 

1793 overlap_delete.append(delete) 

1794 

1795 shape = np.zeros((3, 3, 3)) 

1796 shape[0, :, 2] = 1 

1797 shape[1, :, 1] = 1 

1798 shape[1, :, 0] = 1 

1799 shape[2, 1, 0] = 1 

1800 overlap_shapes.append(shape) 

1801 

1802 delete = np.zeros((3, 3, 3)) 

1803 delete[2, 1, 0] = 1 

1804 overlap_delete.append(delete) 

1805 

1806 shape = np.zeros((3, 3, 3)) 

1807 shape[0, 2, :] = 1 

1808 shape[1, 0:2, :] = 1 

1809 shape[2, 0, 0] = 1 

1810 overlap_shapes.append(shape) 

1811 

1812 delete = np.zeros((3, 3, 3)) 

1813 delete[2, 0, 0] = 1 

1814 overlap_delete.append(delete) 

1815 

1816 shape = np.zeros((3, 3, 3)) 

1817 shape[:, :, 1] = 1 

1818 shape[0, 0, 0] = 1 

1819 overlap_shapes.append(shape) 

1820 

1821 delete = np.zeros((3, 3, 3)) 

1822 delete[0, 0, 0] = 1 

1823 overlap_delete.append(delete) 

1824 

1825 shape = np.zeros((3, 3, 3)) 

1826 shape[0, :, 0] = 1 

1827 shape[1, :, 1] = 1 

1828 shape[1, :, 2] = 1 

1829 shape[1, 1, 0] = 1 

1830 overlap_shapes.append(shape) 

1831 

1832 delete = np.zeros((3, 3, 3)) 

1833 delete[1, 1, 0] = 1 

1834 overlap_delete.append(delete) 

1835 

1836 shape = np.zeros((3, 3, 3)) 

1837 shape[1:3, 2, 0] = 1 

1838 shape[0, 2, 1:3] = 1 

1839 shape[0, 1, 2] = 1 

1840 shape[1, 1, 1] = 1 

1841 shape[2, 1, 0] = 1 

1842 shape[1:3, 0, 1] = 1 

1843 shape[1, 0, 2] = 1 

1844 overlap_shapes.append(shape) 

1845 

1846 delete = np.zeros((3, 3, 3)) 

1847 delete[1, 0, 1] = 1 

1848 overlap_delete.append(delete) 

1849 

1850 # set loop flag 

1851 points_remaining = True 

1852 

1853 # initialise deleted point counter 

1854 deleted_points = 0 

1855 

1856 # set list of possible permutations 

1857 perm_list = [ 

1858 [0, 1, 2], 

1859 [0, 2, 1], 

1860 [1, 2, 0], 

1861 [1, 0, 2], 

1862 [2, 0, 1], 

1863 [2, 1, 0] 

1864 ] 

1865 

1866 while points_remaining: 

1867 

1868 # get linear index of non-zero bowl elements 

1869 index_mat = matlab_find(bowl_sm > 0)[:, 0] 

1870 

1871 # set Boolean delete variable 

1872 delete_point = False 

1873 

1874 # loop through all points on the bowl, and find the all the points with 

1875 # more than 8 neighbours 

1876 index = 0 

1877 for index, index_mat_i in enumerate(index_mat): 

1878 

1879 # extract subscripts for current point 

1880 cx, cy, cz = ind2sub([Nx, Ny, Nz], index_mat_i) 

1881 

1882 # ignore edge points 

1883 if (cx > 1) and (cx < Nx) and (cy > 1) and (cy < Ny) and (cz > 1) and (cz < Nz): 

1884 

1885 # extract local region around current point 

1886 region = bowl_sm[cx - 1:cx + 1, cy - 1:cy + 1, cz - 1:cz + 1] # FARID might not work 

1887 

1888 # if there's more than 8 neighbours, check the point for 

1889 # deletion 

1890 if (region.sum() - 1) > 8: 

1891 

1892 # loop through the different shapes 

1893 for shape_index in range(len(overlap_shapes)): 

1894 

1895 # check every permutation of the shape, and apply the 

1896 # deletion mask if the pattern matches 

1897 

1898 # loop through possible shape permutations 

1899 for ind1 in range(len(perm_list)): 

1900 

1901 # get shape and delete mask 

1902 overlap_s = overlap_shapes[shape_index] 

1903 overlap_d = overlap_delete[shape_index] 

1904 

1905 # permute 

1906 overlap_s = np.transpose(overlap_s, perm_list[ind1]) 

1907 overlap_d = np.transpose(overlap_d, perm_list[ind1]) 

1908 

1909 # loop through possible shape reflections 

1910 for ind2 in range(1, 8): 

1911 

1912 # flipfunc the shape 

1913 if ind2 == 2: 

1914 overlap_s = np.flip(overlap_s, axis=0) 

1915 overlap_d = np.flip(overlap_d, axis=0) 

1916 elif ind2 == 3: 

1917 overlap_s = np.flip(overlap_s, axis=1) 

1918 overlap_d = np.flip(overlap_d, axis=1) 

1919 elif ind2 == 4: 

1920 overlap_s = np.flip(overlap_s, axis=2) 

1921 overlap_d = np.flip(overlap_d, axis=2) 

1922 elif ind2 == 5: 

1923 overlap_s = np.flip(np.flip(overlap_s, axis=0), axis=1) 

1924 overlap_d = np.flip(np.flip(overlap_d, axis=0), axis=1) 

1925 elif ind2 == 6: 

1926 overlap_s = np.flip(np.flip(overlap_s, axis=0), axis=2) 

1927 overlap_d = np.flip(np.flip(overlap_d, axis=0), axis=2) 

1928 elif ind2 == 7: 

1929 overlap_s = np.flip(np.flip(overlap_s, axis=1), axis=2) 

1930 overlap_d = np.flip(np.flip(overlap_d, axis=1), axis=2) 

1931 

1932 # rotate the shape 4 x 90 degrees 

1933 for ind3 in range(4): 

1934 

1935 # check if the shape matches 

1936 if np.all(overlap_s == region): 

1937 delete_point = True 

1938 

1939 # break from loop if a match is found 

1940 if delete_point: 

1941 break 

1942 

1943 # rotate shape 

1944 overlap_s = np.rot90(overlap_s) 

1945 overlap_d = np.rot90(overlap_d) 

1946 

1947 # break from loop if a match is found 

1948 if delete_point: 

1949 break 

1950 

1951 # break from loop if a match is found 

1952 if delete_point: 

1953 break 

1954 

1955 # remove point from bowl if required, and update 

1956 # counter 

1957 if delete_point: 

1958 bowl_sm[cx - 1:cx + 1, cy - 1:cy + 1, cz - 1:cz + 1] = bowl_sm[cx - 1:cx + 1, 

1959 cy - 1:cy + 1, 

1960 cz - 1:cz + 1] * np.bitwise_not( 

1961 overlap_d).astype(float) # Farid won't work probably 

1962 deleted_points = deleted_points + 1 

1963 break 

1964 

1965 # break from loop if a match is found 

1966 if delete_point: 

1967 break 

1968 

1969 # break from while loop if the outer for loop has completed 

1970 # without deleting a point 

1971 if index == (len(index_mat) - 1): 

1972 points_remaining = False 

1973 

1974 # display status 

1975 if deleted_points: 

1976 print('{deleted_points} overlapped points removed from bowl') 

1977 

1978 # ========================================================================= 

1979 # PLACE BOWL WITHIN LARGER GRID 

1980 # ========================================================================= 

1981 

1982 # preallocate storage variable 

1983 if binary: 

1984 bowl = np.zeros(grid_size, dtype=bool) 

1985 else: 

1986 bowl = np.zeros(grid_size) 

1987 

1988 # calculate position of bounding box within larger grid 

1989 x1 = bowl_pos[0] - bx 

1990 x2 = x1 + Nx 

1991 y1 = bowl_pos[1] - by 

1992 y2 = y1 + Ny 

1993 z1 = bowl_pos[2] - bz 

1994 z2 = z1 + Nz 

1995 

1996 # truncate bounding box if it falls outside the grid 

1997 if x1 < 0: 

1998 bowl_sm = bowl_sm[abs(x1):, :, :] 

1999 x1 = 0 

2000 if y1 < 0: 

2001 bowl_sm = bowl_sm[:, abs(y1):, :] 

2002 y1 = 0 

2003 if z1 < 0: 

2004 bowl_sm = bowl_sm[:, :, abs(z1):] 

2005 z1 = 0 

2006 if x2 >= grid_size[0]: 

2007 to_delete = x2 - grid_size[0] 

2008 bowl_sm = bowl_sm[:-to_delete, :, :] 

2009 x2 = grid_size[0] 

2010 if y2 >= grid_size[1]: 

2011 to_delete = y2 - grid_size[1] 

2012 bowl_sm = bowl_sm[:, :-to_delete, :] 

2013 y2 = grid_size[1] 

2014 if z2 >= grid_size[2]: 

2015 to_delete = z2 - grid_size[2] 

2016 bowl_sm = bowl_sm[:, :, :-to_delete] 

2017 z2 = grid_size[2] 

2018 

2019 # place bowl into grid 

2020 bowl[x1:x2, y1:y2, z1:z2] = bowl_sm 

2021 

2022 return bowl 

2023 

2024 

2025def makeMultiBowl(grid_size, bowl_pos, radius, diameter, focus_pos, binary=False, remove_overlap=False): 

2026 # ========================================================================= 

2027 # DEFINE LITERALS 

2028 # ========================================================================= 

2029 

2030 # ========================================================================= 

2031 # INPUT CHECKING 

2032 # ========================================================================= 

2033 

2034 # check inputs 

2035 if bowl_pos.shape[-1] != 3: 

2036 raise ValueError('bowl_pos should contain 3 columns, with [bx, by, bz] in each row.') 

2037 

2038 if len(radius) != 1 and len(radius) != bowl_pos.shape[0]: 

2039 raise ValueError('The number of rows in bowl_pos and radius does not match.') 

2040 

2041 if len(diameter) != 1 and len(diameter) != bowl_pos.shape[0]: 

2042 raise ValueError('The number of rows in bowl_pos and diameter does not match.') 

2043 

2044 # force integer grid size values 

2045 grid_size = np.round(grid_size).astype(int) 

2046 bowl_pos = np.round(bowl_pos).astype(int) 

2047 focus_pos = np.round(focus_pos).astype(int) 

2048 diameter = np.round(diameter) 

2049 radius = np.round(radius) 

2050 

2051 # ========================================================================= 

2052 # CREATE BOWLS 

2053 # ========================================================================= 

2054 

2055 # preallocate output matrices 

2056 if binary: 

2057 bowls = np.zeros(grid_size, dtype=bool) 

2058 else: 

2059 bowls = np.zeros(grid_size) 

2060 

2061 bowls_labelled = np.zeros(grid_size) 

2062 

2063 # loop for calling makeBowl 

2064 for bowl_index in range(bowl_pos.shape[0]): 

2065 

2066 # update command line status 

2067 if bowl_index == 1: 

2068 TicToc.tic() 

2069 else: 

2070 TicToc.toc(reset=True) 

2071 print(f'Creating bowl {bowl_index} of {bowl_pos.shape[0]} ... ') 

2072 

2073 # get parameters for current bowl 

2074 if bowl_pos.shape[0] > 1: 

2075 bowl_pos_k = bowl_pos[bowl_index] 

2076 else: 

2077 bowl_pos_k = bowl_pos 

2078 

2079 if len(radius) > 1: 

2080 radius_k = radius[bowl_index] 

2081 else: 

2082 radius_k = radius 

2083 

2084 if len(diameter) > 1: 

2085 diameter_k = diameter[bowl_index] 

2086 else: 

2087 diameter_k = diameter 

2088 

2089 if focus_pos.shape[0] > 1: 

2090 focus_pos_k = focus_pos[bowl_index] 

2091 else: 

2092 focus_pos_k = focus_pos 

2093 

2094 # create new bowl 

2095 new_bowl = makeBowl( 

2096 grid_size, bowl_pos_k, radius_k, diameter_k, focus_pos_k, 

2097 remove_overlap=remove_overlap, binary=binary 

2098 ) 

2099 

2100 # add bowl to bowl matrix 

2101 bowls = bowls + new_bowl 

2102 

2103 # add new bowl to labelling matrix 

2104 bowls_labelled[new_bowl == 1] = bowl_index 

2105 

2106 TicToc.toc() 

2107 

2108 # check if any of the bowls are overlapping 

2109 max_nd_val, _ = max_nd(bowls) 

2110 if max_nd_val > 1: 

2111 # display warning 

2112 print(f'WARNING: {max_nd_val - 1} bowls are overlapping') 

2113 

2114 # force the output to be binary 

2115 bowls[bowls != 0] = 1 

2116 

2117 return bowls, bowls_labelled 

2118 

2119 

2120def makeMultiArc(grid_size: np.ndarray, arc_pos: np.ndarray, radius, diameter, focus_pos: np.ndarray): 

2121 # check inputs 

2122 if arc_pos.shape[-1] != 2: 

2123 raise ValueError('arc_pos should contain 2 columns, with [ax, ay] in each row.') 

2124 

2125 if len(radius) != 1 and len(radius) != arc_pos.shape[0]: 

2126 raise ValueError('The number of rows in arc_pos and radius does not match.') 

2127 

2128 if len(diameter) != 1 and len(diameter) != arc_pos.shape[0]: 

2129 raise ValueError('The number of rows in arc_pos and diameter does not match.') 

2130 

2131 # force integer grid size values 

2132 grid_size = grid_size.round().astype(int) 

2133 arc_pos = arc_pos.round().astype(int) 

2134 diameter = diameter.round() 

2135 focus_pos = focus_pos.round().astype(int) 

2136 radius = radius.round() 

2137 

2138 # ========================================================================= 

2139 # CREATE ARCS 

2140 # ========================================================================= 

2141 

2142 # create empty matrix 

2143 arcs = np.zeros(grid_size) 

2144 arcs_labelled = np.zeros(grid_size) 

2145 

2146 # loop for calling makeArc 

2147 for k in range(arc_pos.shape[0]): 

2148 

2149 # get parameters for current arc 

2150 if arc_pos.shape[0] > 1: 

2151 arc_pos_k = arc_pos[k] 

2152 else: 

2153 arc_pos_k = arc_pos 

2154 

2155 if len(radius) > 1: 

2156 radius_k = radius[k] 

2157 else: 

2158 radius_k = radius 

2159 

2160 if len(diameter) > 1: 

2161 diameter_k = diameter[k] 

2162 else: 

2163 diameter_k = diameter 

2164 

2165 if focus_pos.shape[0] > 1: 

2166 focus_pos_k = focus_pos[k] 

2167 else: 

2168 focus_pos_k = focus_pos 

2169 

2170 # create new arc 

2171 new_arc = makeArc(grid_size, arc_pos_k, radius_k, diameter_k, focus_pos_k) 

2172 

2173 # add arc to arc matrix 

2174 arcs = arcs + new_arc 

2175 

2176 # add new arc to labelling matrix 

2177 arcs_labelled[new_arc == 1] = k 

2178 

2179 # check if any of the arcs are overlapping 

2180 max_nd_val, _ = max_nd(arcs) 

2181 if max_nd_val > 1: 

2182 # display warning 

2183 print(f'WARNING: {max_nd_val - 1} arcs are overlapping') 

2184 

2185 # force the output to be binary 

2186 arcs[arcs != 0] = 1 

2187 

2188 return arcs, arcs_labelled 

2189 

2190 

2191def makeSphere(Nx, Ny, Nz, radius, plot_sphere=False, binary=False): 

2192 

2193 # enforce a centered sphere 

2194 cx = floor(Nx / 2) + 1 

2195 cy = floor(Ny / 2) + 1 

2196 cz = floor(Nz / 2) + 1 

2197 

2198 # preallocate the storage variable 

2199 if binary: 

2200 sphere = np.zeros((Nx, Ny, Nz), dtype=bool) 

2201 else: 

2202 sphere = np.zeros((Nx, Ny, Nz)) 

2203 

2204 # create a guide circle from which the individal radii can be extracted 

2205 guide_circle = makeCircle(Ny, Nx, cy, cx, radius) 

2206 

2207 # step through the guide circle points and create partially filled discs 

2208 centerpoints = np.arange(cx - radius, cx + 1) 

2209 reflection_offset = np.arange(len(centerpoints), 1, -1) 

2210 for centerpoint_index in range(len(centerpoints)): 

2211 

2212 # extract the current row from the guide circle 

2213 row_data = guide_circle[:, centerpoints[centerpoint_index] - 1] 

2214 

2215 # add an index to the grid points in the current row 

2216 row_index = row_data * np.arange(1, len(row_data) + 1) 

2217 

2218 # calculate the radius 

2219 swept_radius = (row_index.max() - row_index[row_index != 0].min()) / 2 

2220 

2221 # create a circle to add to the sphere 

2222 circle = makeCircle(Ny, Nz, cy, cz, swept_radius) 

2223 

2224 # make an empty fill matrix 

2225 if binary: 

2226 circle_fill = np.zeros((Ny, Nz), dtype=bool) 

2227 else: 

2228 circle_fill = np.zeros((Ny, Nz)) 

2229 

2230 # fill in the circle line by line 

2231 fill_centerpoints = np.arange(cz - swept_radius, cz + swept_radius + 1).astype(int) 

2232 for fill_centerpoints_i in fill_centerpoints: 

2233 

2234 # extract the first row 

2235 row_data = circle[:, fill_centerpoints_i - 1] 

2236 

2237 # add an index to the grid points in the current row 

2238 row_index = row_data * np.arange(1, len(row_data) + 1) 

2239 

2240 # calculate the diameter 

2241 start_index = row_index[row_index != 0].min() 

2242 stop_index = row_index.max() 

2243 

2244 # count how many points on the line 

2245 num_points = sum(row_data) 

2246 

2247 # fill in the line 

2248 if start_index != stop_index and (stop_index - start_index) >= num_points: 

2249 circle_fill[(start_index + num_points // 2) - 1:stop_index - (num_points // 2), fill_centerpoints_i - 1] = 1 

2250 

2251 # remove points from the filled circle that existed in the previous 

2252 # layer 

2253 if centerpoint_index == 0: 

2254 sphere[centerpoints[centerpoint_index] - 1, :, :] = circle + circle_fill 

2255 prev_circle = circle + circle_fill 

2256 else: 

2257 prev_circle_alt = circle + circle_fill 

2258 circle_fill = circle_fill - prev_circle 

2259 circle_fill[circle_fill < 0] = 0 

2260 sphere[centerpoints[centerpoint_index] - 1, :, :] = circle + circle_fill 

2261 prev_circle = prev_circle_alt 

2262 

2263 # create the other half of the sphere at the same time 

2264 if centerpoint_index != len(centerpoints) - 1: 

2265 sphere[cx + reflection_offset[centerpoint_index] - 2, :, :] = sphere[centerpoints[centerpoint_index] - 1, :, :] 

2266 

2267 # plot results 

2268 if plot_sphere: 

2269 raise NotImplementedError 

2270 return sphere 

2271 

2272 

2273def makeSphericalSection(radius, height, width=None, plot_section=False, binary=False): 

2274 use_spherical_sections = True 

2275 

2276 # force inputs to be integers 

2277 radius = int(radius) 

2278 height = int(height) 

2279 

2280 use_width = (width is not None) 

2281 if use_width: 

2282 width = int(width) 

2283 if width % 2 == 0: 

2284 raise ValueError('Input width must be an odd number.') 

2285 

2286 # calculate minimum grid dimensions to fit entire sphere 

2287 Nx = 2*radius + 1 

2288 

2289 # create sphere 

2290 ss = makeSphere(Nx, Nx, Nx, radius, False, binary) 

2291 

2292 # truncate to given height 

2293 if use_spherical_sections: 

2294 ss = ss[:height, :, :] 

2295 else: 

2296 ss = np.transpose(ss[:, :height, :], [1, 2, 0]) 

2297 

2298 # flatten transducer and store the maximum and indices 

2299 mx = np.squeeze(np.max(ss, axis=0)) 

2300 

2301 # calculate the total length/width of the transducer 

2302 length = mx[(len(mx) + 1) // 2].sum() 

2303 

2304 # truncate transducer grid based on length (removes empty rows and columns) 

2305 offset = int((Nx - length)/2) 

2306 ss = ss[:, offset:-offset, offset:-offset] 

2307 

2308 # also truncate to given width if defined by user 

2309 if use_width: 

2310 

2311 # check the value is appropriate 

2312 if width > length: 

2313 raise ValueError('Input for width must be less than or equal to transducer length.') 

2314 

2315 # calculate offset 

2316 offset = int((length - width)/2) 

2317 

2318 # truncate transducer grid 

2319 ss = ss[:, offset:-offset, :] 

2320 

2321 # compute average distance between each grid point and its contiguous 

2322 

2323 # calculate x-index of each grid point in the spherical section, create 

2324 # mask and remove singleton dimensions 

2325 mx, mx_ind = np.max(ss, axis=0), ss.argmax(axis=0) + 1 

2326 mask = np.squeeze(mx != 0) 

2327 mx_ind = np.squeeze(mx_ind) * mask 

2328 

2329 # double check there there is only one value of spherical section in 

2330 # each matrix column 

2331 if mx.sum() != ss.sum(): 

2332 raise ValueError('mean neighbour distance cannot be calculated uniquely due to overlapping points in the x-direction') 

2333 

2334 # calculate average distance to grid point neighbours in the flat case 

2335 x_dist = np.tile([1, 0, 1], [3, 1]) 

2336 y_dist = x_dist.T 

2337 flat_dist = np.sqrt(x_dist**2 + y_dist**2) 

2338 flat_dist = np.mean(flat_dist) 

2339 

2340 # compute distance map 

2341 dist_map = np.zeros(mx_ind.shape) 

2342 sz = mx_ind.shape 

2343 for m in range(sz[0]): 

2344 for n in range(sz[1]): 

2345 

2346 # clear map 

2347 local_heights = np.zeros((3, 3)) 

2348 

2349 # extract the height (x-distance) of the 8 neighbouring grid 

2350 # points 

2351 if m == 0 and n == 0: 

2352 local_heights[1:3, 1:3] = mx_ind[m:m + 2, n:n + 2] 

2353 elif m == (sz[0] - 1) and n == (sz[1] - 1): 

2354 local_heights[0:2, 0:2] = mx_ind[m - 1:m+1, n - 1:n+1] 

2355 elif m == 0 and n == (sz[1] - 1): 

2356 local_heights[1:3, 0:2] = mx_ind[m:m + 2, n - 1:n + 1] 

2357 elif m == (sz[0] - 1) and n == 0: 

2358 local_heights[0:2, 1:3] = mx_ind[m - 1:m+1, n:n + 2] 

2359 elif m == 0: 

2360 local_heights[1:3, :] = mx_ind[m:m + 2, n - 1:n + 2] 

2361 elif m == (sz[0] - 1): 

2362 local_heights[0:2, :] = mx_ind[m - 1:m+1, n - 1:n + 2] 

2363 elif n == 0: 

2364 local_heights[:, 1:3] = mx_ind[m - 1:m + 2, n:n + 2] 

2365 elif n == (sz[1] - 1): 

2366 local_heights[:, 0:2] = mx_ind[m - 1:m + 2, n - 1:n + 1] 

2367 else: 

2368 local_heights = mx_ind[m - 1:m + 2, n - 1:n + 2] 

2369 

2370 # compute average variation from center 

2371 local_heights_var = abs(local_heights - local_heights[1, 1]) 

2372 

2373 # threshold no neighbours 

2374 local_heights_var[local_heights == 0] = 0 

2375 

2376 # calculate total distance from centre 

2377 dist = np.sqrt(x_dist**2 + y_dist**2 + local_heights_var**2) 

2378 

2379 # average and store as a ratio 

2380 dist_map[m, n] = 1 + (np.mean(dist) - flat_dist) / flat_dist 

2381 

2382 # threshold out the non-transducer grid points 

2383 dist_map[mask != 1] = 0 

2384 

2385 # plot if required 

2386 if plot_section: 

2387 raise NotImplementedError 

2388 

2389 return ss, dist_map