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

1 import math

2 from math import floor

3 from typing import Tuple, Optional

4

5 import matplotlib.pyplot as plt

6 import numpy as np

7 from kwave.utils.tictoc import TicToc

8

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

10 from kwave.utils.conversionutils import scale_SI

11 from scipy import optimize

12 import warnings

13

14 from kwave.utils.conversionutils import db2neper, neper2db

15

16

17 def 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

47 def 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

56 def 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

118 def 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

160 def 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

209 def 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

233 def 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

271 def 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

304 def 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

338 def 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

376 def 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

444 def 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

493 def 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

540 def 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

602 def 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

705 def 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

750 def 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

843 def 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

884 def 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

1260 def 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

1359 def 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

1369 def 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

1378 def 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

1426 def 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

1473 def 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

2025 def 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

2120 def 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

2191 def 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

2273 def 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