src.acoustools.Visualiser
1import torch 2from acoustools.Utilities import propagate_abs, device, TRANSDUCERS, create_points 3import matplotlib.pyplot as plt 4from mpl_toolkits.mplot3d import art3d 5import matplotlib.colors as clrs 6import matplotlib.cm as cm 7import matplotlib.colors as mcolors 8from mpl_toolkits.axes_grid1 import make_axes_locatable 9from matplotlib.widgets import MultiCursor 10 11import matplotlib.animation as animation 12import matplotlib as mpl 13 14from torch import Tensor 15from types import FunctionType 16from typing import Literal 17from vedo import Mesh 18 19 20 21 22 23def Visualise(A:Tensor,B:Tensor,C:Tensor,activation:Tensor,points:list[Tensor]|Tensor=[], 24 colour_functions:list[FunctionType]|None=[propagate_abs], colour_function_args:list[dict]|None=None, 25 res:tuple[int]=(200,200), cmaps:list[str]=[], add_lines_functions:list[FunctionType]|None=None, 26 add_line_args:list[dict]|None=None,vmin:int|list[int]|None=None,vmax:int|list[int]|None=None, 27 matricies:Tensor|list[Tensor]|None = None, show:bool=True,block:bool=True, clr_labels:list[str]|None=None, depth:int=2, link_ax:str|list|None='all', 28 cursor:bool=False, arrangement:tuple|None = None, arangement:tuple|None = None, titles:list[str]|None=None, call_abs=False, norm_axes=None ) -> None: 29 ''' 30 Visualises any number of fields generated from activation to the plane ABC and arranges them in a (1,N) grid \n 31 :param A: Position of the top left corner of the image 32 :param B: Position of the top right corner of the image 33 :param C: Position of the bottom left corner of the image 34 :param activation: The transducer activation to use 35 :param points: List of point positions to add crosses for each plot. Positions should be given in their position in 3D 36 :param colour_functions: List of function to call at each position for each plot. Should return a value to colour the pixel at that position. Default `acoustools.Utilities.propagate_abs`. \n 37 - If colour_functions is `difference` or `ratio` then the result will be the difference or ratio between existing images. To control which images are used `colour_function_args` should have a parameter called `ids`. 38 :param colour_function_args: The arguments to pass to `colour_functions` 39 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 40 :param cmaps: The cmaps to pass to plot 41 :param add_lines_functions: List of functions to extract lines and add to the image 42 :param add_line_args: List of parameters to add to `add_lines_functions` 43 :param vmin: Minimum value to use across all plots 44 :param vmax: MAximum value to use across all plots 45 :param matricies: precomputed matricies to plot 46 :param show: If True will call `plt.show(block=block)` else does not. Default True 47 :param block: Will be passed to `plot.show(block=block)`. Default True 48 :param clr_label: Label for colourbar 49 :param depth: Number of times to tile image 50 :param link_ax: Axes to link colourbar of `'all'` to link all axes. To unlink all axis, pass one of ['none', False, None] 51 :param cursor: If `True` will show cursor across plots 52 :param arangement: Arangment of subplots 53 :param title: Titles for each subplot 54 :param call_abs: if True will call torch.abs on the image 55 :param norm_axes: List of Axes to Normalise 56 57 ```Python 58 from acoustools.Utilities import create_points, add_lev_sig 59 from acoustools.Solvers import wgs 60 from acoustools.Visualiser import Visualise 61 62 import torch 63 64 p = create_points(1,1,x=0,y=0,z=0) 65 x = wgs(p) 66 x = add_lev_sig(x) 67 68 A = torch.tensor((-0.09,0, 0.09)) 69 B = torch.tensor((0.09,0, 0.09)) 70 C = torch.tensor((-0.09,0, -0.09)) 71 normal = (0,1,0) 72 origin = (0,0,0) 73 74 Visualise(A,B,C, x, points=p) 75 ``` 76 ''' 77 78 axs =[] 79 results = [] 80 lines = [] 81 82 if arangement is None and arrangement is not None: 83 arangement = arrangement 84 85 86 if colour_function_args is None and colour_functions is not None: 87 colour_function_args = [{}]*len(colour_functions) 88 89 if type(activation) is not list : 90 activation = [activation] *len(colour_functions) 91 92 if type(A) != list: 93 A = [A] * len(colour_functions) 94 if type(B) != list: 95 B = [B] * len(colour_functions) 96 if type(C) != list: 97 C = [C] * len(colour_functions) 98 99 if colour_functions is not None: 100 for i,colour_function in enumerate(colour_functions): 101 if type(colour_function) is not str: 102 if depth > 0: 103 result = Visualise_single_blocks(A[i],B[i],C[i],activation[i],colour_function, colour_function_args[i], res, depth=depth) 104 else: 105 result = Visualise_single(A[i],B[i],C[i],activation[i],colour_function, colour_function_args[i], res) 106 results.append(result) 107 108 if add_lines_functions is not None: 109 if add_lines_functions[i] is not None: 110 lines.append(add_lines_functions[i](**add_line_args[i])) 111 else: 112 lines.append(None) 113 else: 114 i1, i2 = colour_function_args[i]['ids'] if (i<len(colour_function_args) and 'ids' in colour_function_args[i]) else (0,1) 115 img1 = results[i1] 116 img2 = results[i2] 117 118 if colour_function.lower() == 'difference' or colour_function.lower() == 'diff' or colour_function.lower() == '-': 119 image = (img2 - img1) 120 if colour_function.lower() == 'ratio' or colour_function.lower() == 'rat' or colour_function.lower() == '/': 121 image = (img2 / img1) 122 123 post_function= colour_function_args[i]['post_function'] if (i<len(colour_function_args) and 'post_function' in colour_function_args[i]) else None 124 if post_function is not None: 125 image = post_function(image) 126 127 results.append(image) 128 129 130 else: 131 for i,mat in enumerate(matricies): 132 result = mat 133 results.append(result) 134 135 if add_lines_functions is not None: 136 if add_lines_functions[i] is not None: 137 lines.append(add_lines_functions[i](**add_line_args[i])) 138 else: 139 lines.append(None) 140 141 v_min = vmin 142 v_max = vmax 143 144 if type(vmax) is list: 145 v_max = vmax[0] 146 147 if type(vmin) is list: 148 v_min = vmin[0] 149 150 norms = {} 151 152 if link_ax == 'all' or link_ax == True: 153 norm = mcolors.Normalize(vmin=v_min, vmax=v_max) 154 for i in range(len(results)): 155 norms[i] = norm 156 elif link_ax == 'none' or link_ax is None or link_ax == False: 157 for i in range(len(results)): 158 mn = vmin[i] if (vmin is not None and i < len(vmin)) else None 159 mx = vmax[i] if (vmax is not None and i < len(vmax)) else None 160 norms[i] = mcolors.Normalize(vmin=mn, vmax=mx) 161 162 else: 163 if type(link_ax[0]) == list or type(link_ax[0]) == tuple: 164 for group in link_ax: 165 norm = mcolors.Normalize(vmin=v_min, vmax=v_max) 166 for i in range(len(results)): 167 if i in group: 168 norms[i] = norm 169 elif i not in norms: 170 norms[i] = None 171 172 else: 173 norm = mcolors.Normalize(vmin=v_min, vmax=v_max) 174 group = link_ax 175 for i in range(len(results)): 176 if i in group: 177 norms[i] = norm 178 elif i not in norms: 179 norms[i] = None 180 181 182 for i in range(len(results)): 183 if len(cmaps) > 0: 184 cmap = cmaps[i] 185 else: 186 cmap = 'hot' 187 188 length = len(colour_functions) if colour_functions is not None else len(matricies) 189 if arangement is None: 190 arangement = (1,length) 191 if i > 0: 192 ax = plt.subplot(*arangement,i+1, sharex = ax, sharey=ax) 193 else: 194 ax = plt.subplot(*arangement,i+1) 195 196 if titles is not None: 197 t = titles[i] 198 if t is not None: 199 ax.set_title(t) 200 201 axs.append(ax) 202 im = results[i] 203 if call_abs: im = torch.abs(im) 204 205 206 # if v_min is None: 207 # v_min = torch.min(im) 208 # if v_max is None: 209 # v_max = torch.max(im) 210 211 212 # print(vmax,vmin) 213 214 if clr_labels is None: 215 clr_label = 'Pressure (Pa)' 216 else: 217 clr_label = clr_labels[i] 218 219 220 if norm_axes is not None and i in norm_axes: 221 im = im / torch.max(im) 222 img = plt.imshow(im.cpu().detach().numpy(),cmap=cmap,norm=norms[i]) 223 plt.yticks([]) 224 plt.xticks([]) 225 226 if len(points) >0: 227 228 if type(points) == list: 229 points = torch.concatenate(points,axis=0) 230 pts_pos = get_point_pos(A[i],B[i],C[i],points.real,res) 231 # print(pts_pos) 232 pts_pos_t = torch.stack(pts_pos).T 233 234 ax.scatter(pts_pos_t[1],pts_pos_t[0],marker="x") 235 236 237 238 if im.shape[2] == 1: 239 divider = make_axes_locatable(ax) 240 cax = divider.append_axes("right", size="5%", pad=0.05) 241 242 plt.colorbar(label=clr_label,cax=cax) 243 else: 244 divider = make_axes_locatable(ax) 245 cax = divider.append_axes("right", size="0%", pad=0.05) 246 cax.set_xticks([]) 247 cax.set_yticks([]) 248 249 250 if add_lines_functions is not None: 251 AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]) 252 AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]) 253 # print(AB,AC) 254 norm_x = AB 255 norm_y = AC 256 AB = AB[AB!=0] / res[0] 257 AC = AC[AC!=0] / res[1] 258 # AC = AC / torch.abs(AC) 259 # print(AB,AC) 260 if lines[i] is not None: 261 for con in lines[i]: 262 xs = [con[0][0]/AB + res[0]/2, con[1][0]/AB + res[0]/2] #Convert real coordinates to pixels - number of steps in each direction 263 ys = [con[0][1]/AC + res[1]/2, con[1][1]/AC + res[1]/2] #Add res/2 as 0,0,0 in middle of real coordinates not corner of image 264 # print(xs,ys) 265 plt.plot(xs,ys,color = "blue") 266 267 268 269 fig = plt.gcf() 270 271 c = MultiCursor(fig.canvas, axs, color='b',lw=0.5, horizOn=True, vertOn=True) 272 273 multi_event_id = fig.canvas.mpl_connect('motion_notify_event', c.onmove) 274 275 def press(event): 276 nonlocal multi_event_id 277 if event.key == 'z': 278 if c.visible: 279 fig.canvas.mpl_disconnect(multi_event_id) 280 fig.canvas.draw_idle() 281 c.visible = False 282 c.active = True 283 else: 284 multi_event_id = fig.canvas.mpl_connect('motion_notify_event', c.onmove) 285 fig.canvas.draw_idle() 286 for line in c.vlines + c.hlines: 287 line.set_visible(True) 288 c.visible = True 289 if event.key == 'x' and c.visible: 290 c.active = not c.active 291 292 fig.canvas.mpl_connect('key_press_event', press) 293 294 if not cursor: 295 fig.canvas.mpl_disconnect(multi_event_id) 296 fig.canvas.draw_idle() 297 c.visible = False 298 c.active = True 299 300 301 302 if show: 303 plt.show(block=block) 304 else: 305 return fig 306 307 308def get_point_pos(A:Tensor,B:Tensor,C:Tensor, points:Tensor, res:tuple[int]=(200,200),flip:bool=True) -> list[int]: 309 ''' 310 converts point positions in 3D to pixel locations in the plane defined by ABC\n 311 :param A: Position of the top left corner of the image 312 :param B: Position of the top right corner of the image 313 :param C: Position of the bottom left corner of the image 314 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 315 :param flip: Reverses X and Y directions. Default True 316 :return: List of point positions 317 ''' 318 AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]) 319 AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]) 320 321 ab_dir = AB!=0 322 ac_dir = AC!=0 323 324 step_x = AB / res[0] 325 step_y = AC / res[1] 326 327 if points.shape[2] > 1: 328 points = torch.split(points.squeeze().T,1) 329 points = [pt.squeeze() for pt in points] 330 # print(points) 331 332 pts_norm = [] 333 334 for pt in points: 335 Apt = torch.tensor([pt[0] - A[0], pt[1] - A[1], pt[2] - A[2]]) 336 px = Apt / step_x 337 py = Apt / step_y 338 pt_pos = torch.zeros((2)) 339 if not flip: 340 pt_pos[0]= torch.round(px[ab_dir]) 341 pt_pos[1]=torch.round(py[ac_dir]) 342 else: 343 pt_pos[1]= torch.round(px[ab_dir]) 344 pt_pos[0]=torch.round(py[ac_dir]) 345 346 pts_norm.append(pt_pos) 347 348 349 350 return pts_norm 351 352def get_image_positions(A:Tensor,B:Tensor,C:Tensor, res:tuple[int]=(200,200)): 353 ''' 354 Gets res[0] x res[1] points in the plane defined by ABC 355 :param A: Position of the top left corner of the image 356 :param B: Position of the top right corner of the image 357 :param C: Position of the bottom left corner of the image 358 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 359 :returnns positions: The positions of pixels 360 ''' 361 AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]).to(device) 362 AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]).to(device) 363 364 step_x = AB / res[0] 365 step_y = AC / res[1] 366 367 positions = torch.zeros((1,3,res[0]*res[1])).to(device) 368 369 for i in range(0,res[0]): 370 for j in range(res[1]): 371 positions[:,:,i*res[1]+j] = A + step_x * i + step_y * j 372 373 return positions 374 375 376def Visualise_single(A:Tensor,B:Tensor,C:Tensor,activation:Tensor, 377 colour_function:FunctionType=propagate_abs, colour_function_args:dict={}, 378 res:tuple[int]=(200,200), flip:bool=True) -> Tensor: 379 ''' 380 Visalises field generated from activation to the plane ABC 381 :param A: Position of the top left corner of the image 382 :param B: Position of the top right corner of the image 383 :param C: Position of the bottom left corner of the image 384 :param activation: The transducer activation to use 385 :param colour_function: Function to call at each position. Should return a numeric value to colour the pixel at that position. Default `acoustools.Utilities.propagate_abs` 386 :param colour_function_args: The arguments to pass to `colour_function` 387 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 388 :param flip: Reverses X and Y directions. Default True 389 :return: Tensor of values of propagated field 390 ''' 391 if len(activation.shape) < 3: 392 activation = activation.unsqueeze(0) 393 394 395 396 positions = get_image_positions(A,B,C,res) 397 398 399 # print(positions.shape) 400 # print(colour_function_args) 401 field_val = colour_function(activations=activation,points=positions,**colour_function_args) 402 if len(field_val.shape) < 3: 403 field_val.unsqueeze_(2) 404 results = [] 405 for i in range(field_val.shape[2]): 406 result = torch.reshape(field_val[:,:,i], res) 407 results.append(result) 408 result = torch.stack(results,dim=2) 409 410 if flip: 411 result = torch.rot90(torch.fliplr(result)) 412 413 414 return result 415 416def force_quiver(points: Tensor, U:Tensor,V:Tensor,norm:tuple[int]=(0,0,0), ylims:int|None=None, xlims:int|None=None, 417 log:bool=False,show:bool=True,colour:str|None=None, reciprocal:bool = False, block:bool=True, scale:float=1) -> None: 418 ''' 419 Plot the force on a mesh as a quiver plot\n 420 :param points: The centre of the mesh faces 421 :param U: Force in first axis 422 :param V: Force in second axis 423 :param norm: 424 :param ylims: limit of y axis 425 :param zlims: limit of x axis 426 :param log: if `True` take the log of the values before plotting 427 :param show: if `True` call `plt.show()` 428 :param colour: colour of arrows 429 :param reciprocal: if `True` plot reciprocal of values 430 :param block: passed into `plt.show` 431 :param scale: value to scale arrows by - note will pass 1/scale to matplotlib 432 ''' 433 434 B = points.shape[0] 435 N = points.shape[2] 436 437 # if len(points) > 0: 438 # pts_pos = get_point_pos(A,B,C,points,res) 439 440 mask = ~(torch.tensor(norm).to(bool)) 441 points = points[:,mask,:] 442 # points=torch.reshape(points,(B,2,-1)) 443 444 445 xs = points[:,0,:].cpu().detach().numpy()[0] 446 ys = points[:,1,:].cpu().detach().numpy()[0] 447 448 449 if log: 450 U = torch.sign(U) * torch.abs(torch.log(torch.abs(U))) 451 V = torch.sign(V) * torch.abs(torch.log(torch.abs(V))) 452 453 if reciprocal: 454 U = 1/U 455 V = 1/V 456 457 458 plt.quiver(xs, ys, U.cpu().detach().numpy(),V.cpu().detach().numpy(),color = colour,linewidths=0.01,scale=1/scale) 459 plt.axis('equal') 460 461 462 if ylims is not None: 463 plt.ylim(ylims[0],ylims[1]) 464 465 if xlims is not None: 466 plt.xlim(xlims[0],xlims[1]) 467 468 if show: 469 plt.show(block=block) 470 471 472 473def force_quiver_3d(points:Tensor, U:Tensor,V:Tensor,W:Tensor, scale:float=1, show:bool=True, ax:mpl.axes.Axes|None=None) ->None: 474 ''' 475 Plot the force on a mesh in 3D 476 :param points: The centre of the mesh faces 477 :param U: Force in first axis 478 :param V: Force in second axis 479 :param W: Force in third axis 480 :param scale: value to scale result by 481 :param show: If True will call `plt.show()` 482 :param ax: Axis to use 483 ''' 484 485 if ax is None: ax = plt.figure().add_subplot(projection='3d') 486 ax.quiver(points[:,0,:].cpu().detach().numpy(), points[:,1,:].cpu().detach().numpy(), points[:,2,:].cpu().detach().numpy(), U.cpu().detach().numpy()* scale, V.cpu().detach().numpy()* scale, W.cpu().detach().numpy()* scale) 487 488 if show: plt.show() 489 else: return ax 490 491 492 493 494def Visualise_mesh(mesh:Mesh, colours:Tensor|None=None, points:Tensor|None=None, p_pressure:Tensor|None=None, 495 vmax:int|None=None,vmin:int|None=None, show:bool=True, subplot:int|plt.Axes|None=None, fig:plt.Figure|None=None, 496 buffer_x:int=0, buffer_y:int = 0, buffer_z:int = 0, equalise_axis:bool=False, elev:float=-45, azim:float=45, 497 clamp:bool=False) ->None: 498 ''' 499 Plot a mesh in 3D and colour the mesh faces 500 :param mesh: Mesh to plot 501 :param colours: Colours for each face 502 :param points: Positions of points to also plot 503 :param p_pressure: Values to colour points with 504 :param vmax: Maximum colour to plot 505 :param vmin: Minimum colour to plot 506 :param show: If `True` call `plot.show()` 507 :param subplot: Optionally use existing subplot 508 :param fig: Optionally use existing fig 509 :param buffer_x: Amount of whitesapce to add in x direction 510 :param buffer_y: Amount of whitesapce to add in y direction 511 :param buffer_z: Amount of whitesapce to add in z direction 512 :param equalise_axis: If `True` call `ax.set_aspect('equal')` 513 :param elev: elevation angle 514 :param azim: azimuth angle 515 :param clamp: if True will clamp values in colours to vmax and vmin 516 ''' 517 518 xmin,xmax, ymin,ymax, zmin,zmax = mesh.bounds() 519 520 if type(colours) is torch.Tensor: 521 colours=colours.flatten() 522 523 if clamp: 524 colours = torch.clamp(colours,vmin,vmax) 525 526 527 v = mesh.vertices 528 f = torch.tensor(mesh.cells) 529 530 if fig is None: 531 fig = plt.figure() 532 533 if subplot is None: 534 ax = fig.add_subplot(projection="3d") 535 else: 536 ax = fig.add_subplot(subplot,projection="3d") 537 538 # norm = plt.Normalize(C.min(), C.max()) 539 # colors = plt.cm.viridis(norm(C)) 540 541 if vmin is None and colours is not None: 542 vmin = torch.min(colours).item() 543 if p_pressure is not None and p_pressure < vmin: 544 vmin = p_pressure 545 546 if vmax is None and colours is not None: 547 vmax = torch.max(colours).item() 548 if p_pressure is not None and p_pressure > vmax: 549 vmax = p_pressure 550 551 norm = clrs.Normalize(vmin=vmin, vmax=vmax, clip=True) 552 mapper = cm.ScalarMappable(norm, cmap=cm.hot) 553 554 if points is not None: 555 if p_pressure is not None: 556 p_c = mapper.to_rgba(p_pressure.squeeze().cpu().detach()) 557 else: 558 p_c = 'blue' 559 points = points.cpu().detach() 560 ax.scatter(points[:,0],points[:,1],points[:,2],color=p_c) 561 562 if colours is not None: 563 colour_mapped = [] 564 for c in colours: 565 colour_mapped.append(mapper.to_rgba(c.cpu().detach())) 566 else: 567 colour_mapped=None 568 569 pc = art3d.Poly3DCollection(v[f], edgecolor="black", linewidth=0.01, facecolors=colour_mapped) 570 plt_3d = ax.add_collection(pc) 571 572 573 mappable = cm.ScalarMappable(cmap=cm.hot, norm=norm) 574 mappable.set_array(colour_mapped) # Link the data to the ScalarMappable 575 576 # Add the color bar to the figure 577 cbar = fig.colorbar(mappable, ax=ax, shrink=0.6) 578 cbar.set_label('Face Value') 579 580 581 scale = mesh.vertices.flatten() 582 ax.auto_scale_xyz(scale, scale, scale) 583 584 585 if not equalise_axis: 586 ax.set_xlim([xmin - buffer_x, xmax + buffer_x]) 587 ax.set_ylim([ymin - buffer_y, ymax + buffer_y]) 588 ax.set_zlim([zmin - buffer_z, zmax + buffer_z]) 589 else: 590 ax.set_aspect('equal') 591 592 593 ax.view_init(elev=elev, azim=azim) 594 595 596 597 if show: 598 plt.show() 599 else: 600 return ax 601 602 603 604 605def Visualise_line(A:Tensor,B:Tensor,x:Tensor, F:Tensor|None=None,points:Tensor|None=None,steps:int = 1000, 606 board:Tensor|None=None, propagate_fun:FunctionType = propagate_abs, propagate_args:dict={}, show:bool=True) -> None: 607 ''' 608 Plot the field across a line from A->B\n 609 :param A: Start of line 610 :param B: End of line 611 :param x: Hologram 612 :param F: Optionally, propagation matrix 613 :param points: Optionally, pass the points on line AB instead of computing them 614 :param steps: Number of points along line 615 :param board: Transducers to use 616 :param propagate_fun: Function to use to propagate hologram 617 :propagate_args: arguments for `propagate_fun` 618 :show: If `True` call `plt.show()` 619 ''' 620 if board is None: 621 board = TRANSDUCERS 622 623 if points is None: 624 AB = B-A 625 step = AB / steps 626 points = [] 627 for i in range(steps): 628 p = A + i*step 629 points.append(p.unsqueeze(0)) 630 631 points = torch.stack(points, 2).to(device) 632 633 634 pressure = propagate_fun(activations=x,points=points, board=board,**propagate_args) 635 if show: 636 plt.plot(pressure.detach().cpu().flatten()) 637 plt.show() 638 else: 639 return pressure 640 641 642def ABC(size:float, plane:Literal['xz', 'yz', 'xy'] = 'xz', origin:Tensor|tuple=None) -> tuple[Tensor]: 643 ''' 644 Get ABC values for visualisation 645 * A top right corner 646 * B top right corner 647 * C bottom left corner 648 :param size: The size of the window 649 :param plane: Plane, one of 'xz' 'yz' 'xy' 650 :param origin: The centre of the view window 651 :return: A,B,C 652 ''' 653 if origin is None: 654 origin = torch.tensor((0,0,0), device=device) 655 if type(origin) == tuple or type(origin) == list: 656 origin = torch.tensor(origin).real 657 658 origin = origin.squeeze().real 659 660 661 if plane == 'xz': 662 A = origin+torch.tensor((-1,0, 1), device=device) * size 663 B = origin+torch.tensor((1,0, 1), device=device)* size 664 C = origin+torch.tensor((-1,0, -1), device=device)* size 665 666 if plane == 'yz': 667 A = origin+torch.tensor((0,-1, 1), device=device) * size 668 B = origin+torch.tensor((0,1, 1), device=device)* size 669 C = origin+torch.tensor((0,-1, -1), device=device)* size 670 671 if plane == 'xy': 672 A = origin+torch.tensor((-1,1, 0), device=device) * size 673 B = origin+torch.tensor((1, 1,0), device=device)* size 674 C = origin+torch.tensor((-1, -1,0), device=device)* size 675 676 677 678 return A.to(device), B.to(device), C.to(device) 679 680def ABC_2_tiles(A:Tensor,B:Tensor,C:Tensor): 681 ''' 682 Split ABC defined region into 4 tiles 683 * A top right corner 684 * B top right corner 685 * C bottom left corner 686 687 ''' 688 A1 = A 689 B1 = A + (B-A)/2 690 C1 = A+ (C-A)/2 691 692 A2 = A+ (B-A)/2 693 B2 = B 694 C2 = A+ ((B-A)/2 + (C-A)/2) 695 696 A3 = A+ (C-A)/2 697 B3 = A + ((B-A)/2 + (C-A)/2) 698 C3 = C 699 700 A4 = A + ((B-A)/2 + (C-A)/2) 701 B4 = A + (B-A)+(C-A)/2 702 C4 = A + ((B-A)/2 + (C-A)) 703 704 return (A1,B1,C1), (A2,B2,C2), (A3,B3,C3), (A4,B4,C4) 705 706def combine_tiles(t1:Tensor,t2:Tensor,t3:Tensor,t4:Tensor): 707 ''' 708 Combines subimages into a larger image, used in `Visualise_single_blocks` 709 :param t1: Top left image 710 :param t2: Top right image 711 :param t3: Bottom left image 712 :param t4: Bottom right image 713 ''' 714 top = torch.cat([t1,t2],dim=1) 715 bottom = torch.cat([t3,t4],dim=1) 716 717 return torch.cat([top,bottom],dim=0) 718 719def Visualise_single_blocks(A:Tensor,B:Tensor,C:Tensor,activation:Tensor, 720 colour_function:FunctionType=propagate_abs, colour_function_args:dict={}, 721 res:tuple[int]=(200,200), flip:bool=True, depth=2) -> Tensor: 722 ''' 723 Visalises field generated from activation to the plane ABC in a slightly nicer memory efficient way by chunking into tiles 724 :param A: Position of the top left corner of the image 725 :param B: Position of the top right corner of the image 726 :param C: Position of the bottom left corner of the image 727 :param activation: The transducer activation to use 728 :param colour_function: Function to call at each position. Should return a numeric value to colour the pixel at that position. Default `acoustools.Utilities.propagate_abs` 729 :param colour_function_args: The arguments to pass to `colour_function` 730 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 731 :param flip: Reverses X and Y directions. Default True 732 :param depth: Number of times to chunk 733 :return: Tensor of values of propagated field 734 ''' 735 736 tiles = ABC_2_tiles(A,B,C) 737 738 new_res = (int(res[0]/2), int(res[1]/2)) 739 740 ims = [] 741 742 743 for (nA,nB,nC) in tiles: 744 if depth == 1: 745 im = Visualise_single(nA,nB,nC,activation,colour_function=colour_function, colour_function_args=colour_function_args, res=new_res, flip=flip) 746 else: 747 im = Visualise_single_blocks(nA,nB,nC,activation,colour_function=colour_function, colour_function_args=colour_function_args, res=new_res, flip=flip, depth = depth-1) 748 ims.append(im) 749 # torch.cuda.empty_cache() 750 751 im = combine_tiles(*ims) 752 return im 753 754 755def animate_lcode(pth, ax:mpl.axes.Axes|None=None, fig:plt.Figure=None, skip:int=1, show:bool=False, 756 fname:str='', extruder:Tensor|None = None, xlims:tuple[float]|None=None, 757 ylims:tuple[float]|None=None, zlims:tuple[float]|None=None, dpi:int=100, interval:int = 1, 758 legend:bool=True, title:bool=True) -> None: 759 ''' 760 Reads a .lcode file and produces a gif of the simulation of the result of that lcode\n 761 :param pth: Path to the .lcode file 762 :param ax: Axis to use, if None will create new 763 :param fig: Figure to use, if None will create new 764 :param skip: Number of instructions to skip per animation frame, default 1 (no skipping) 765 :param show: If true will call plt.show() 766 :param: fname: Name of file to save to 767 :param extruder: If not None the position of the extruder to plot as Tensor 768 :param xlims: Tuple of xlims, if None will use (-0.12,0.12) 769 :param ylims: Tuple of ylims, if None will use (-0.12,0.12) 770 :param zlims: Tuple of zlims, if None will use (-0.12,0.12) 771 :param dpi: dpi to use when saving gif 772 :param inetrval: Time to wait between frames 773 :param legend: If True will add figure legend 774 :param title: If True will add figure title 775 ''' 776 777 if fig is None: fig = plt.figure() 778 if ax is None: ax = fig.add_subplot(projection='3d') 779 780 781 point_commands = ['L0','L1','L2','L3'] 782 783 frames = [] 784 printed_points = [[],] 785 786 functions = {} 787 in_function = None 788 789 790 791 with open(pth,'r') as file: 792 lines = file.readlines() 793 LINES = len(lines) 794 for i,line in enumerate(lines): 795 print(f"Line {i}/{LINES}", end='\r') 796 line = line.replace(';','').rstrip() 797 split = line.split(':') 798 cmd = split[0] 799 800 if cmd == 'function': 801 name = split[1] 802 functions[name] = [] 803 in_function = name 804 805 elif cmd == 'end': 806 name = split[1] 807 in_function = None 808 809 elif cmd.startswith('F'): 810 frame_points = functions[cmd] 811 for frame in frame_points: 812 frames.append(frame) 813 frame_printed_points = printed_points[-1].copy() 814 printed_points.append(frame_printed_points) 815 816 817 818 elif cmd in point_commands: 819 points = split[1:] 820 ps = [] 821 for point in points: 822 ps.append(point.split(',')) 823 824 frame_points = [[float(p) for p in pt] for pt in ps] 825 frames.append(frame_points) 826 827 if in_function is not None: 828 functions[in_function].append(frame_points) 829 830 frame_printed_points = printed_points[-1].copy() 831 if cmd == 'C1': 832 frame_printed_points.append(frames[-1].copy()) 833 834 printed_points.append(frame_printed_points) 835 836 837 if extruder is not None: 838 if type(extruder) is bool: 839 extruder = create_points(1,1,0,-0.04, 0.04) 840 ex_x = extruder[:,0].detach().cpu() 841 ex_y = extruder[:,1].detach().cpu() 842 ex_z = extruder[:,2].detach().cpu() 843 844 845 FRAMES = int(len(frames) / skip) 846 # FRAMES = 100 847 848 if xlims is None: 849 xlims = (-0.12,0.12) 850 851 if ylims is None: 852 ylims = (-0.12,0.12) 853 854 if zlims is None: 855 zlims = (-0.12,0.12) 856 857 858 def traverse(index): 859 index = index*skip 860 ax.clear() 861 print(f"Line {index/skip}/{FRAMES}", end='\r') 862 for pt in frames[index]: 863 ax.scatter(*pt, label='Trap') 864 865 printed_xs = [i for i in [[p[0] for p in pt] for pt in printed_points[index]]] 866 printed_ys = [i for i in [[p[1] for p in pt] for pt in printed_points[index]]] 867 printed_zs = [i for i in [[p[2] for p in pt] for pt in printed_points[index]]] 868 869 ax.scatter(printed_xs,printed_ys, printed_zs, label='Printed', edgecolor='black') 870 871 ax.set_ylim(xlims) 872 ax.set_xlim(ylims) 873 ax.set_zlim(zlims) 874 875 876 if extruder is not None: ax.scatter(ex_x, ex_y, ex_z, label='Extruder') 877 878 if legend: ax.legend() 879 if title: ax.set_title(f'Location: {index}') 880 881 882 883 ani = animation.FuncAnimation(fig, traverse, frames=FRAMES, interval=interval) 884 if show: plt.show() 885 886 if fname == '': 887 fname = 'Results.gif' 888 ani.save(fname, writer='imagemagick', dpi = dpi)
24def Visualise(A:Tensor,B:Tensor,C:Tensor,activation:Tensor,points:list[Tensor]|Tensor=[], 25 colour_functions:list[FunctionType]|None=[propagate_abs], colour_function_args:list[dict]|None=None, 26 res:tuple[int]=(200,200), cmaps:list[str]=[], add_lines_functions:list[FunctionType]|None=None, 27 add_line_args:list[dict]|None=None,vmin:int|list[int]|None=None,vmax:int|list[int]|None=None, 28 matricies:Tensor|list[Tensor]|None = None, show:bool=True,block:bool=True, clr_labels:list[str]|None=None, depth:int=2, link_ax:str|list|None='all', 29 cursor:bool=False, arrangement:tuple|None = None, arangement:tuple|None = None, titles:list[str]|None=None, call_abs=False, norm_axes=None ) -> None: 30 ''' 31 Visualises any number of fields generated from activation to the plane ABC and arranges them in a (1,N) grid \n 32 :param A: Position of the top left corner of the image 33 :param B: Position of the top right corner of the image 34 :param C: Position of the bottom left corner of the image 35 :param activation: The transducer activation to use 36 :param points: List of point positions to add crosses for each plot. Positions should be given in their position in 3D 37 :param colour_functions: List of function to call at each position for each plot. Should return a value to colour the pixel at that position. Default `acoustools.Utilities.propagate_abs`. \n 38 - If colour_functions is `difference` or `ratio` then the result will be the difference or ratio between existing images. To control which images are used `colour_function_args` should have a parameter called `ids`. 39 :param colour_function_args: The arguments to pass to `colour_functions` 40 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 41 :param cmaps: The cmaps to pass to plot 42 :param add_lines_functions: List of functions to extract lines and add to the image 43 :param add_line_args: List of parameters to add to `add_lines_functions` 44 :param vmin: Minimum value to use across all plots 45 :param vmax: MAximum value to use across all plots 46 :param matricies: precomputed matricies to plot 47 :param show: If True will call `plt.show(block=block)` else does not. Default True 48 :param block: Will be passed to `plot.show(block=block)`. Default True 49 :param clr_label: Label for colourbar 50 :param depth: Number of times to tile image 51 :param link_ax: Axes to link colourbar of `'all'` to link all axes. To unlink all axis, pass one of ['none', False, None] 52 :param cursor: If `True` will show cursor across plots 53 :param arangement: Arangment of subplots 54 :param title: Titles for each subplot 55 :param call_abs: if True will call torch.abs on the image 56 :param norm_axes: List of Axes to Normalise 57 58 ```Python 59 from acoustools.Utilities import create_points, add_lev_sig 60 from acoustools.Solvers import wgs 61 from acoustools.Visualiser import Visualise 62 63 import torch 64 65 p = create_points(1,1,x=0,y=0,z=0) 66 x = wgs(p) 67 x = add_lev_sig(x) 68 69 A = torch.tensor((-0.09,0, 0.09)) 70 B = torch.tensor((0.09,0, 0.09)) 71 C = torch.tensor((-0.09,0, -0.09)) 72 normal = (0,1,0) 73 origin = (0,0,0) 74 75 Visualise(A,B,C, x, points=p) 76 ``` 77 ''' 78 79 axs =[] 80 results = [] 81 lines = [] 82 83 if arangement is None and arrangement is not None: 84 arangement = arrangement 85 86 87 if colour_function_args is None and colour_functions is not None: 88 colour_function_args = [{}]*len(colour_functions) 89 90 if type(activation) is not list : 91 activation = [activation] *len(colour_functions) 92 93 if type(A) != list: 94 A = [A] * len(colour_functions) 95 if type(B) != list: 96 B = [B] * len(colour_functions) 97 if type(C) != list: 98 C = [C] * len(colour_functions) 99 100 if colour_functions is not None: 101 for i,colour_function in enumerate(colour_functions): 102 if type(colour_function) is not str: 103 if depth > 0: 104 result = Visualise_single_blocks(A[i],B[i],C[i],activation[i],colour_function, colour_function_args[i], res, depth=depth) 105 else: 106 result = Visualise_single(A[i],B[i],C[i],activation[i],colour_function, colour_function_args[i], res) 107 results.append(result) 108 109 if add_lines_functions is not None: 110 if add_lines_functions[i] is not None: 111 lines.append(add_lines_functions[i](**add_line_args[i])) 112 else: 113 lines.append(None) 114 else: 115 i1, i2 = colour_function_args[i]['ids'] if (i<len(colour_function_args) and 'ids' in colour_function_args[i]) else (0,1) 116 img1 = results[i1] 117 img2 = results[i2] 118 119 if colour_function.lower() == 'difference' or colour_function.lower() == 'diff' or colour_function.lower() == '-': 120 image = (img2 - img1) 121 if colour_function.lower() == 'ratio' or colour_function.lower() == 'rat' or colour_function.lower() == '/': 122 image = (img2 / img1) 123 124 post_function= colour_function_args[i]['post_function'] if (i<len(colour_function_args) and 'post_function' in colour_function_args[i]) else None 125 if post_function is not None: 126 image = post_function(image) 127 128 results.append(image) 129 130 131 else: 132 for i,mat in enumerate(matricies): 133 result = mat 134 results.append(result) 135 136 if add_lines_functions is not None: 137 if add_lines_functions[i] is not None: 138 lines.append(add_lines_functions[i](**add_line_args[i])) 139 else: 140 lines.append(None) 141 142 v_min = vmin 143 v_max = vmax 144 145 if type(vmax) is list: 146 v_max = vmax[0] 147 148 if type(vmin) is list: 149 v_min = vmin[0] 150 151 norms = {} 152 153 if link_ax == 'all' or link_ax == True: 154 norm = mcolors.Normalize(vmin=v_min, vmax=v_max) 155 for i in range(len(results)): 156 norms[i] = norm 157 elif link_ax == 'none' or link_ax is None or link_ax == False: 158 for i in range(len(results)): 159 mn = vmin[i] if (vmin is not None and i < len(vmin)) else None 160 mx = vmax[i] if (vmax is not None and i < len(vmax)) else None 161 norms[i] = mcolors.Normalize(vmin=mn, vmax=mx) 162 163 else: 164 if type(link_ax[0]) == list or type(link_ax[0]) == tuple: 165 for group in link_ax: 166 norm = mcolors.Normalize(vmin=v_min, vmax=v_max) 167 for i in range(len(results)): 168 if i in group: 169 norms[i] = norm 170 elif i not in norms: 171 norms[i] = None 172 173 else: 174 norm = mcolors.Normalize(vmin=v_min, vmax=v_max) 175 group = link_ax 176 for i in range(len(results)): 177 if i in group: 178 norms[i] = norm 179 elif i not in norms: 180 norms[i] = None 181 182 183 for i in range(len(results)): 184 if len(cmaps) > 0: 185 cmap = cmaps[i] 186 else: 187 cmap = 'hot' 188 189 length = len(colour_functions) if colour_functions is not None else len(matricies) 190 if arangement is None: 191 arangement = (1,length) 192 if i > 0: 193 ax = plt.subplot(*arangement,i+1, sharex = ax, sharey=ax) 194 else: 195 ax = plt.subplot(*arangement,i+1) 196 197 if titles is not None: 198 t = titles[i] 199 if t is not None: 200 ax.set_title(t) 201 202 axs.append(ax) 203 im = results[i] 204 if call_abs: im = torch.abs(im) 205 206 207 # if v_min is None: 208 # v_min = torch.min(im) 209 # if v_max is None: 210 # v_max = torch.max(im) 211 212 213 # print(vmax,vmin) 214 215 if clr_labels is None: 216 clr_label = 'Pressure (Pa)' 217 else: 218 clr_label = clr_labels[i] 219 220 221 if norm_axes is not None and i in norm_axes: 222 im = im / torch.max(im) 223 img = plt.imshow(im.cpu().detach().numpy(),cmap=cmap,norm=norms[i]) 224 plt.yticks([]) 225 plt.xticks([]) 226 227 if len(points) >0: 228 229 if type(points) == list: 230 points = torch.concatenate(points,axis=0) 231 pts_pos = get_point_pos(A[i],B[i],C[i],points.real,res) 232 # print(pts_pos) 233 pts_pos_t = torch.stack(pts_pos).T 234 235 ax.scatter(pts_pos_t[1],pts_pos_t[0],marker="x") 236 237 238 239 if im.shape[2] == 1: 240 divider = make_axes_locatable(ax) 241 cax = divider.append_axes("right", size="5%", pad=0.05) 242 243 plt.colorbar(label=clr_label,cax=cax) 244 else: 245 divider = make_axes_locatable(ax) 246 cax = divider.append_axes("right", size="0%", pad=0.05) 247 cax.set_xticks([]) 248 cax.set_yticks([]) 249 250 251 if add_lines_functions is not None: 252 AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]) 253 AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]) 254 # print(AB,AC) 255 norm_x = AB 256 norm_y = AC 257 AB = AB[AB!=0] / res[0] 258 AC = AC[AC!=0] / res[1] 259 # AC = AC / torch.abs(AC) 260 # print(AB,AC) 261 if lines[i] is not None: 262 for con in lines[i]: 263 xs = [con[0][0]/AB + res[0]/2, con[1][0]/AB + res[0]/2] #Convert real coordinates to pixels - number of steps in each direction 264 ys = [con[0][1]/AC + res[1]/2, con[1][1]/AC + res[1]/2] #Add res/2 as 0,0,0 in middle of real coordinates not corner of image 265 # print(xs,ys) 266 plt.plot(xs,ys,color = "blue") 267 268 269 270 fig = plt.gcf() 271 272 c = MultiCursor(fig.canvas, axs, color='b',lw=0.5, horizOn=True, vertOn=True) 273 274 multi_event_id = fig.canvas.mpl_connect('motion_notify_event', c.onmove) 275 276 def press(event): 277 nonlocal multi_event_id 278 if event.key == 'z': 279 if c.visible: 280 fig.canvas.mpl_disconnect(multi_event_id) 281 fig.canvas.draw_idle() 282 c.visible = False 283 c.active = True 284 else: 285 multi_event_id = fig.canvas.mpl_connect('motion_notify_event', c.onmove) 286 fig.canvas.draw_idle() 287 for line in c.vlines + c.hlines: 288 line.set_visible(True) 289 c.visible = True 290 if event.key == 'x' and c.visible: 291 c.active = not c.active 292 293 fig.canvas.mpl_connect('key_press_event', press) 294 295 if not cursor: 296 fig.canvas.mpl_disconnect(multi_event_id) 297 fig.canvas.draw_idle() 298 c.visible = False 299 c.active = True 300 301 302 303 if show: 304 plt.show(block=block) 305 else: 306 return fig
Visualises any number of fields generated from activation to the plane ABC and arranges them in a (1,N) grid
Parameters
- A: Position of the top left corner of the image
- B: Position of the top right corner of the image
- C: Position of the bottom left corner of the image
- activation: The transducer activation to use
- points: List of point positions to add crosses for each plot. Positions should be given in their position in 3D
colour_functions: List of function to call at each position for each plot. Should return a value to colour the pixel at that position. Default
acoustools.Utilities.propagate_abs.- If colour_functions is
differenceorratiothen the result will be the difference or ratio between existing images. To control which images are usedcolour_function_argsshould have a parameter calledids.
- If colour_functions is
- colour_function_args: The arguments to pass to
colour_functions - res: Number of pixels as a tuple (X,Y). Default (200,200)
- cmaps: The cmaps to pass to plot
- add_lines_functions: List of functions to extract lines and add to the image
- add_line_args: List of parameters to add to
add_lines_functions - vmin: Minimum value to use across all plots
- vmax: MAximum value to use across all plots
- matricies: precomputed matricies to plot
- show: If True will call
plt.show(block=block)else does not. Default True - block: Will be passed to
plot.show(block=block). Default True - clr_label: Label for colourbar
- depth: Number of times to tile image
- link_ax: Axes to link colourbar of
'all'to link all axes. To unlink all axis, pass one of ['none', False, None] - cursor: If
Truewill show cursor across plots - arangement: Arangment of subplots
- title: Titles for each subplot
- call_abs: if True will call torch.abs on the image
- norm_axes: List of Axes to Normalise
from acoustools.Utilities import create_points, add_lev_sig
from acoustools.Solvers import wgs
from acoustools.Visualiser import Visualise
import torch
p = create_points(1,1,x=0,y=0,z=0)
x = wgs(p)
x = add_lev_sig(x)
A = torch.tensor((-0.09,0, 0.09))
B = torch.tensor((0.09,0, 0.09))
C = torch.tensor((-0.09,0, -0.09))
normal = (0,1,0)
origin = (0,0,0)
Visualise(A,B,C, x, points=p)
309def get_point_pos(A:Tensor,B:Tensor,C:Tensor, points:Tensor, res:tuple[int]=(200,200),flip:bool=True) -> list[int]: 310 ''' 311 converts point positions in 3D to pixel locations in the plane defined by ABC\n 312 :param A: Position of the top left corner of the image 313 :param B: Position of the top right corner of the image 314 :param C: Position of the bottom left corner of the image 315 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 316 :param flip: Reverses X and Y directions. Default True 317 :return: List of point positions 318 ''' 319 AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]) 320 AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]) 321 322 ab_dir = AB!=0 323 ac_dir = AC!=0 324 325 step_x = AB / res[0] 326 step_y = AC / res[1] 327 328 if points.shape[2] > 1: 329 points = torch.split(points.squeeze().T,1) 330 points = [pt.squeeze() for pt in points] 331 # print(points) 332 333 pts_norm = [] 334 335 for pt in points: 336 Apt = torch.tensor([pt[0] - A[0], pt[1] - A[1], pt[2] - A[2]]) 337 px = Apt / step_x 338 py = Apt / step_y 339 pt_pos = torch.zeros((2)) 340 if not flip: 341 pt_pos[0]= torch.round(px[ab_dir]) 342 pt_pos[1]=torch.round(py[ac_dir]) 343 else: 344 pt_pos[1]= torch.round(px[ab_dir]) 345 pt_pos[0]=torch.round(py[ac_dir]) 346 347 pts_norm.append(pt_pos) 348 349 350 351 return pts_norm
converts point positions in 3D to pixel locations in the plane defined by ABC
Parameters
- A: Position of the top left corner of the image
- B: Position of the top right corner of the image
- C: Position of the bottom left corner of the image
- res: Number of pixels as a tuple (X,Y). Default (200,200)
- flip: Reverses X and Y directions. Default True
Returns
List of point positions
353def get_image_positions(A:Tensor,B:Tensor,C:Tensor, res:tuple[int]=(200,200)): 354 ''' 355 Gets res[0] x res[1] points in the plane defined by ABC 356 :param A: Position of the top left corner of the image 357 :param B: Position of the top right corner of the image 358 :param C: Position of the bottom left corner of the image 359 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 360 :returnns positions: The positions of pixels 361 ''' 362 AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]).to(device) 363 AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]).to(device) 364 365 step_x = AB / res[0] 366 step_y = AC / res[1] 367 368 positions = torch.zeros((1,3,res[0]*res[1])).to(device) 369 370 for i in range(0,res[0]): 371 for j in range(res[1]): 372 positions[:,:,i*res[1]+j] = A + step_x * i + step_y * j 373 374 return positions
Gets res[0] x res[1] points in the plane defined by ABC
Parameters
- A: Position of the top left corner of the image
- B: Position of the top right corner of the image
- C: Position of the bottom left corner of the image
- res: Number of pixels as a tuple (X,Y). Default (200,200) :returnns positions: The positions of pixels
377def Visualise_single(A:Tensor,B:Tensor,C:Tensor,activation:Tensor, 378 colour_function:FunctionType=propagate_abs, colour_function_args:dict={}, 379 res:tuple[int]=(200,200), flip:bool=True) -> Tensor: 380 ''' 381 Visalises field generated from activation to the plane ABC 382 :param A: Position of the top left corner of the image 383 :param B: Position of the top right corner of the image 384 :param C: Position of the bottom left corner of the image 385 :param activation: The transducer activation to use 386 :param colour_function: Function to call at each position. Should return a numeric value to colour the pixel at that position. Default `acoustools.Utilities.propagate_abs` 387 :param colour_function_args: The arguments to pass to `colour_function` 388 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 389 :param flip: Reverses X and Y directions. Default True 390 :return: Tensor of values of propagated field 391 ''' 392 if len(activation.shape) < 3: 393 activation = activation.unsqueeze(0) 394 395 396 397 positions = get_image_positions(A,B,C,res) 398 399 400 # print(positions.shape) 401 # print(colour_function_args) 402 field_val = colour_function(activations=activation,points=positions,**colour_function_args) 403 if len(field_val.shape) < 3: 404 field_val.unsqueeze_(2) 405 results = [] 406 for i in range(field_val.shape[2]): 407 result = torch.reshape(field_val[:,:,i], res) 408 results.append(result) 409 result = torch.stack(results,dim=2) 410 411 if flip: 412 result = torch.rot90(torch.fliplr(result)) 413 414 415 return result
Visalises field generated from activation to the plane ABC
Parameters
- A: Position of the top left corner of the image
- B: Position of the top right corner of the image
- C: Position of the bottom left corner of the image
- activation: The transducer activation to use
- colour_function: Function to call at each position. Should return a numeric value to colour the pixel at that position. Default
acoustools.Utilities.propagate_abs - colour_function_args: The arguments to pass to
colour_function - res: Number of pixels as a tuple (X,Y). Default (200,200)
- flip: Reverses X and Y directions. Default True
Returns
Tensor of values of propagated field
417def force_quiver(points: Tensor, U:Tensor,V:Tensor,norm:tuple[int]=(0,0,0), ylims:int|None=None, xlims:int|None=None, 418 log:bool=False,show:bool=True,colour:str|None=None, reciprocal:bool = False, block:bool=True, scale:float=1) -> None: 419 ''' 420 Plot the force on a mesh as a quiver plot\n 421 :param points: The centre of the mesh faces 422 :param U: Force in first axis 423 :param V: Force in second axis 424 :param norm: 425 :param ylims: limit of y axis 426 :param zlims: limit of x axis 427 :param log: if `True` take the log of the values before plotting 428 :param show: if `True` call `plt.show()` 429 :param colour: colour of arrows 430 :param reciprocal: if `True` plot reciprocal of values 431 :param block: passed into `plt.show` 432 :param scale: value to scale arrows by - note will pass 1/scale to matplotlib 433 ''' 434 435 B = points.shape[0] 436 N = points.shape[2] 437 438 # if len(points) > 0: 439 # pts_pos = get_point_pos(A,B,C,points,res) 440 441 mask = ~(torch.tensor(norm).to(bool)) 442 points = points[:,mask,:] 443 # points=torch.reshape(points,(B,2,-1)) 444 445 446 xs = points[:,0,:].cpu().detach().numpy()[0] 447 ys = points[:,1,:].cpu().detach().numpy()[0] 448 449 450 if log: 451 U = torch.sign(U) * torch.abs(torch.log(torch.abs(U))) 452 V = torch.sign(V) * torch.abs(torch.log(torch.abs(V))) 453 454 if reciprocal: 455 U = 1/U 456 V = 1/V 457 458 459 plt.quiver(xs, ys, U.cpu().detach().numpy(),V.cpu().detach().numpy(),color = colour,linewidths=0.01,scale=1/scale) 460 plt.axis('equal') 461 462 463 if ylims is not None: 464 plt.ylim(ylims[0],ylims[1]) 465 466 if xlims is not None: 467 plt.xlim(xlims[0],xlims[1]) 468 469 if show: 470 plt.show(block=block)
Plot the force on a mesh as a quiver plot
Parameters
- points: The centre of the mesh faces
- U: Force in first axis
- V: Force in second axis
- norm:
- ylims: limit of y axis
- zlims: limit of x axis
- log: if
Truetake the log of the values before plotting - show: if
Truecallplt.show() - colour: colour of arrows
- reciprocal: if
Trueplot reciprocal of values - block: passed into
plt.show - scale: value to scale arrows by - note will pass 1/scale to matplotlib
474def force_quiver_3d(points:Tensor, U:Tensor,V:Tensor,W:Tensor, scale:float=1, show:bool=True, ax:mpl.axes.Axes|None=None) ->None: 475 ''' 476 Plot the force on a mesh in 3D 477 :param points: The centre of the mesh faces 478 :param U: Force in first axis 479 :param V: Force in second axis 480 :param W: Force in third axis 481 :param scale: value to scale result by 482 :param show: If True will call `plt.show()` 483 :param ax: Axis to use 484 ''' 485 486 if ax is None: ax = plt.figure().add_subplot(projection='3d') 487 ax.quiver(points[:,0,:].cpu().detach().numpy(), points[:,1,:].cpu().detach().numpy(), points[:,2,:].cpu().detach().numpy(), U.cpu().detach().numpy()* scale, V.cpu().detach().numpy()* scale, W.cpu().detach().numpy()* scale) 488 489 if show: plt.show() 490 else: return ax
Plot the force on a mesh in 3D
Parameters
- points: The centre of the mesh faces
- U: Force in first axis
- V: Force in second axis
- W: Force in third axis
- scale: value to scale result by
- show: If True will call
plt.show() - ax: Axis to use
495def Visualise_mesh(mesh:Mesh, colours:Tensor|None=None, points:Tensor|None=None, p_pressure:Tensor|None=None, 496 vmax:int|None=None,vmin:int|None=None, show:bool=True, subplot:int|plt.Axes|None=None, fig:plt.Figure|None=None, 497 buffer_x:int=0, buffer_y:int = 0, buffer_z:int = 0, equalise_axis:bool=False, elev:float=-45, azim:float=45, 498 clamp:bool=False) ->None: 499 ''' 500 Plot a mesh in 3D and colour the mesh faces 501 :param mesh: Mesh to plot 502 :param colours: Colours for each face 503 :param points: Positions of points to also plot 504 :param p_pressure: Values to colour points with 505 :param vmax: Maximum colour to plot 506 :param vmin: Minimum colour to plot 507 :param show: If `True` call `plot.show()` 508 :param subplot: Optionally use existing subplot 509 :param fig: Optionally use existing fig 510 :param buffer_x: Amount of whitesapce to add in x direction 511 :param buffer_y: Amount of whitesapce to add in y direction 512 :param buffer_z: Amount of whitesapce to add in z direction 513 :param equalise_axis: If `True` call `ax.set_aspect('equal')` 514 :param elev: elevation angle 515 :param azim: azimuth angle 516 :param clamp: if True will clamp values in colours to vmax and vmin 517 ''' 518 519 xmin,xmax, ymin,ymax, zmin,zmax = mesh.bounds() 520 521 if type(colours) is torch.Tensor: 522 colours=colours.flatten() 523 524 if clamp: 525 colours = torch.clamp(colours,vmin,vmax) 526 527 528 v = mesh.vertices 529 f = torch.tensor(mesh.cells) 530 531 if fig is None: 532 fig = plt.figure() 533 534 if subplot is None: 535 ax = fig.add_subplot(projection="3d") 536 else: 537 ax = fig.add_subplot(subplot,projection="3d") 538 539 # norm = plt.Normalize(C.min(), C.max()) 540 # colors = plt.cm.viridis(norm(C)) 541 542 if vmin is None and colours is not None: 543 vmin = torch.min(colours).item() 544 if p_pressure is not None and p_pressure < vmin: 545 vmin = p_pressure 546 547 if vmax is None and colours is not None: 548 vmax = torch.max(colours).item() 549 if p_pressure is not None and p_pressure > vmax: 550 vmax = p_pressure 551 552 norm = clrs.Normalize(vmin=vmin, vmax=vmax, clip=True) 553 mapper = cm.ScalarMappable(norm, cmap=cm.hot) 554 555 if points is not None: 556 if p_pressure is not None: 557 p_c = mapper.to_rgba(p_pressure.squeeze().cpu().detach()) 558 else: 559 p_c = 'blue' 560 points = points.cpu().detach() 561 ax.scatter(points[:,0],points[:,1],points[:,2],color=p_c) 562 563 if colours is not None: 564 colour_mapped = [] 565 for c in colours: 566 colour_mapped.append(mapper.to_rgba(c.cpu().detach())) 567 else: 568 colour_mapped=None 569 570 pc = art3d.Poly3DCollection(v[f], edgecolor="black", linewidth=0.01, facecolors=colour_mapped) 571 plt_3d = ax.add_collection(pc) 572 573 574 mappable = cm.ScalarMappable(cmap=cm.hot, norm=norm) 575 mappable.set_array(colour_mapped) # Link the data to the ScalarMappable 576 577 # Add the color bar to the figure 578 cbar = fig.colorbar(mappable, ax=ax, shrink=0.6) 579 cbar.set_label('Face Value') 580 581 582 scale = mesh.vertices.flatten() 583 ax.auto_scale_xyz(scale, scale, scale) 584 585 586 if not equalise_axis: 587 ax.set_xlim([xmin - buffer_x, xmax + buffer_x]) 588 ax.set_ylim([ymin - buffer_y, ymax + buffer_y]) 589 ax.set_zlim([zmin - buffer_z, zmax + buffer_z]) 590 else: 591 ax.set_aspect('equal') 592 593 594 ax.view_init(elev=elev, azim=azim) 595 596 597 598 if show: 599 plt.show() 600 else: 601 return ax
Plot a mesh in 3D and colour the mesh faces
Parameters
- mesh: Mesh to plot
- colours: Colours for each face
- points: Positions of points to also plot
- p_pressure: Values to colour points with
- vmax: Maximum colour to plot
- vmin: Minimum colour to plot
- show: If
Truecallplot.show() - subplot: Optionally use existing subplot
- fig: Optionally use existing fig
- buffer_x: Amount of whitesapce to add in x direction
- buffer_y: Amount of whitesapce to add in y direction
- buffer_z: Amount of whitesapce to add in z direction
- equalise_axis: If
Truecallax.set_aspect('equal') - elev: elevation angle
- azim: azimuth angle
- clamp: if True will clamp values in colours to vmax and vmin
606def Visualise_line(A:Tensor,B:Tensor,x:Tensor, F:Tensor|None=None,points:Tensor|None=None,steps:int = 1000, 607 board:Tensor|None=None, propagate_fun:FunctionType = propagate_abs, propagate_args:dict={}, show:bool=True) -> None: 608 ''' 609 Plot the field across a line from A->B\n 610 :param A: Start of line 611 :param B: End of line 612 :param x: Hologram 613 :param F: Optionally, propagation matrix 614 :param points: Optionally, pass the points on line AB instead of computing them 615 :param steps: Number of points along line 616 :param board: Transducers to use 617 :param propagate_fun: Function to use to propagate hologram 618 :propagate_args: arguments for `propagate_fun` 619 :show: If `True` call `plt.show()` 620 ''' 621 if board is None: 622 board = TRANSDUCERS 623 624 if points is None: 625 AB = B-A 626 step = AB / steps 627 points = [] 628 for i in range(steps): 629 p = A + i*step 630 points.append(p.unsqueeze(0)) 631 632 points = torch.stack(points, 2).to(device) 633 634 635 pressure = propagate_fun(activations=x,points=points, board=board,**propagate_args) 636 if show: 637 plt.plot(pressure.detach().cpu().flatten()) 638 plt.show() 639 else: 640 return pressure
Plot the field across a line from A->B
Parameters
- A: Start of line
- B: End of line
- x: Hologram
- F: Optionally, propagation matrix
- points: Optionally, pass the points on line AB instead of computing them
- steps: Number of points along line
- board: Transducers to use
- propagate_fun: Function to use to propagate hologram
:propagate_args: arguments for
propagate_fun:show: IfTruecallplt.show()
643def ABC(size:float, plane:Literal['xz', 'yz', 'xy'] = 'xz', origin:Tensor|tuple=None) -> tuple[Tensor]: 644 ''' 645 Get ABC values for visualisation 646 * A top right corner 647 * B top right corner 648 * C bottom left corner 649 :param size: The size of the window 650 :param plane: Plane, one of 'xz' 'yz' 'xy' 651 :param origin: The centre of the view window 652 :return: A,B,C 653 ''' 654 if origin is None: 655 origin = torch.tensor((0,0,0), device=device) 656 if type(origin) == tuple or type(origin) == list: 657 origin = torch.tensor(origin).real 658 659 origin = origin.squeeze().real 660 661 662 if plane == 'xz': 663 A = origin+torch.tensor((-1,0, 1), device=device) * size 664 B = origin+torch.tensor((1,0, 1), device=device)* size 665 C = origin+torch.tensor((-1,0, -1), device=device)* size 666 667 if plane == 'yz': 668 A = origin+torch.tensor((0,-1, 1), device=device) * size 669 B = origin+torch.tensor((0,1, 1), device=device)* size 670 C = origin+torch.tensor((0,-1, -1), device=device)* size 671 672 if plane == 'xy': 673 A = origin+torch.tensor((-1,1, 0), device=device) * size 674 B = origin+torch.tensor((1, 1,0), device=device)* size 675 C = origin+torch.tensor((-1, -1,0), device=device)* size 676 677 678 679 return A.to(device), B.to(device), C.to(device)
Get ABC values for visualisation
- A top right corner
- B top right corner
- C bottom left corner
Parameters
- size: The size of the window
- plane: Plane, one of 'xz' 'yz' 'xy'
- origin: The centre of the view window
Returns
A,B,C
681def ABC_2_tiles(A:Tensor,B:Tensor,C:Tensor): 682 ''' 683 Split ABC defined region into 4 tiles 684 * A top right corner 685 * B top right corner 686 * C bottom left corner 687 688 ''' 689 A1 = A 690 B1 = A + (B-A)/2 691 C1 = A+ (C-A)/2 692 693 A2 = A+ (B-A)/2 694 B2 = B 695 C2 = A+ ((B-A)/2 + (C-A)/2) 696 697 A3 = A+ (C-A)/2 698 B3 = A + ((B-A)/2 + (C-A)/2) 699 C3 = C 700 701 A4 = A + ((B-A)/2 + (C-A)/2) 702 B4 = A + (B-A)+(C-A)/2 703 C4 = A + ((B-A)/2 + (C-A)) 704 705 return (A1,B1,C1), (A2,B2,C2), (A3,B3,C3), (A4,B4,C4)
Split ABC defined region into 4 tiles
- A top right corner
- B top right corner
- C bottom left corner
707def combine_tiles(t1:Tensor,t2:Tensor,t3:Tensor,t4:Tensor): 708 ''' 709 Combines subimages into a larger image, used in `Visualise_single_blocks` 710 :param t1: Top left image 711 :param t2: Top right image 712 :param t3: Bottom left image 713 :param t4: Bottom right image 714 ''' 715 top = torch.cat([t1,t2],dim=1) 716 bottom = torch.cat([t3,t4],dim=1) 717 718 return torch.cat([top,bottom],dim=0)
Combines subimages into a larger image, used in Visualise_single_blocks
Parameters
- t1: Top left image
- t2: Top right image
- t3: Bottom left image
- t4: Bottom right image
720def Visualise_single_blocks(A:Tensor,B:Tensor,C:Tensor,activation:Tensor, 721 colour_function:FunctionType=propagate_abs, colour_function_args:dict={}, 722 res:tuple[int]=(200,200), flip:bool=True, depth=2) -> Tensor: 723 ''' 724 Visalises field generated from activation to the plane ABC in a slightly nicer memory efficient way by chunking into tiles 725 :param A: Position of the top left corner of the image 726 :param B: Position of the top right corner of the image 727 :param C: Position of the bottom left corner of the image 728 :param activation: The transducer activation to use 729 :param colour_function: Function to call at each position. Should return a numeric value to colour the pixel at that position. Default `acoustools.Utilities.propagate_abs` 730 :param colour_function_args: The arguments to pass to `colour_function` 731 :param res: Number of pixels as a tuple (X,Y). Default (200,200) 732 :param flip: Reverses X and Y directions. Default True 733 :param depth: Number of times to chunk 734 :return: Tensor of values of propagated field 735 ''' 736 737 tiles = ABC_2_tiles(A,B,C) 738 739 new_res = (int(res[0]/2), int(res[1]/2)) 740 741 ims = [] 742 743 744 for (nA,nB,nC) in tiles: 745 if depth == 1: 746 im = Visualise_single(nA,nB,nC,activation,colour_function=colour_function, colour_function_args=colour_function_args, res=new_res, flip=flip) 747 else: 748 im = Visualise_single_blocks(nA,nB,nC,activation,colour_function=colour_function, colour_function_args=colour_function_args, res=new_res, flip=flip, depth = depth-1) 749 ims.append(im) 750 # torch.cuda.empty_cache() 751 752 im = combine_tiles(*ims) 753 return im
Visalises field generated from activation to the plane ABC in a slightly nicer memory efficient way by chunking into tiles
Parameters
- A: Position of the top left corner of the image
- B: Position of the top right corner of the image
- C: Position of the bottom left corner of the image
- activation: The transducer activation to use
- colour_function: Function to call at each position. Should return a numeric value to colour the pixel at that position. Default
acoustools.Utilities.propagate_abs - colour_function_args: The arguments to pass to
colour_function - res: Number of pixels as a tuple (X,Y). Default (200,200)
- flip: Reverses X and Y directions. Default True
- depth: Number of times to chunk
Returns
Tensor of values of propagated field
756def animate_lcode(pth, ax:mpl.axes.Axes|None=None, fig:plt.Figure=None, skip:int=1, show:bool=False, 757 fname:str='', extruder:Tensor|None = None, xlims:tuple[float]|None=None, 758 ylims:tuple[float]|None=None, zlims:tuple[float]|None=None, dpi:int=100, interval:int = 1, 759 legend:bool=True, title:bool=True) -> None: 760 ''' 761 Reads a .lcode file and produces a gif of the simulation of the result of that lcode\n 762 :param pth: Path to the .lcode file 763 :param ax: Axis to use, if None will create new 764 :param fig: Figure to use, if None will create new 765 :param skip: Number of instructions to skip per animation frame, default 1 (no skipping) 766 :param show: If true will call plt.show() 767 :param: fname: Name of file to save to 768 :param extruder: If not None the position of the extruder to plot as Tensor 769 :param xlims: Tuple of xlims, if None will use (-0.12,0.12) 770 :param ylims: Tuple of ylims, if None will use (-0.12,0.12) 771 :param zlims: Tuple of zlims, if None will use (-0.12,0.12) 772 :param dpi: dpi to use when saving gif 773 :param inetrval: Time to wait between frames 774 :param legend: If True will add figure legend 775 :param title: If True will add figure title 776 ''' 777 778 if fig is None: fig = plt.figure() 779 if ax is None: ax = fig.add_subplot(projection='3d') 780 781 782 point_commands = ['L0','L1','L2','L3'] 783 784 frames = [] 785 printed_points = [[],] 786 787 functions = {} 788 in_function = None 789 790 791 792 with open(pth,'r') as file: 793 lines = file.readlines() 794 LINES = len(lines) 795 for i,line in enumerate(lines): 796 print(f"Line {i}/{LINES}", end='\r') 797 line = line.replace(';','').rstrip() 798 split = line.split(':') 799 cmd = split[0] 800 801 if cmd == 'function': 802 name = split[1] 803 functions[name] = [] 804 in_function = name 805 806 elif cmd == 'end': 807 name = split[1] 808 in_function = None 809 810 elif cmd.startswith('F'): 811 frame_points = functions[cmd] 812 for frame in frame_points: 813 frames.append(frame) 814 frame_printed_points = printed_points[-1].copy() 815 printed_points.append(frame_printed_points) 816 817 818 819 elif cmd in point_commands: 820 points = split[1:] 821 ps = [] 822 for point in points: 823 ps.append(point.split(',')) 824 825 frame_points = [[float(p) for p in pt] for pt in ps] 826 frames.append(frame_points) 827 828 if in_function is not None: 829 functions[in_function].append(frame_points) 830 831 frame_printed_points = printed_points[-1].copy() 832 if cmd == 'C1': 833 frame_printed_points.append(frames[-1].copy()) 834 835 printed_points.append(frame_printed_points) 836 837 838 if extruder is not None: 839 if type(extruder) is bool: 840 extruder = create_points(1,1,0,-0.04, 0.04) 841 ex_x = extruder[:,0].detach().cpu() 842 ex_y = extruder[:,1].detach().cpu() 843 ex_z = extruder[:,2].detach().cpu() 844 845 846 FRAMES = int(len(frames) / skip) 847 # FRAMES = 100 848 849 if xlims is None: 850 xlims = (-0.12,0.12) 851 852 if ylims is None: 853 ylims = (-0.12,0.12) 854 855 if zlims is None: 856 zlims = (-0.12,0.12) 857 858 859 def traverse(index): 860 index = index*skip 861 ax.clear() 862 print(f"Line {index/skip}/{FRAMES}", end='\r') 863 for pt in frames[index]: 864 ax.scatter(*pt, label='Trap') 865 866 printed_xs = [i for i in [[p[0] for p in pt] for pt in printed_points[index]]] 867 printed_ys = [i for i in [[p[1] for p in pt] for pt in printed_points[index]]] 868 printed_zs = [i for i in [[p[2] for p in pt] for pt in printed_points[index]]] 869 870 ax.scatter(printed_xs,printed_ys, printed_zs, label='Printed', edgecolor='black') 871 872 ax.set_ylim(xlims) 873 ax.set_xlim(ylims) 874 ax.set_zlim(zlims) 875 876 877 if extruder is not None: ax.scatter(ex_x, ex_y, ex_z, label='Extruder') 878 879 if legend: ax.legend() 880 if title: ax.set_title(f'Location: {index}') 881 882 883 884 ani = animation.FuncAnimation(fig, traverse, frames=FRAMES, interval=interval) 885 if show: plt.show() 886 887 if fname == '': 888 fname = 'Results.gif' 889 ani.save(fname, writer='imagemagick', dpi = dpi)
Reads a .lcode file and produces a gif of the simulation of the result of that lcode
Parameters
- pth: Path to the .lcode file
- ax: Axis to use, if None will create new
- fig: Figure to use, if None will create new
- skip: Number of instructions to skip per animation frame, default 1 (no skipping)
- show: If true will call plt.show()
- fname: Name of file to save to
- extruder: If not None the position of the extruder to plot as Tensor
- xlims: Tuple of xlims, if None will use (-0.12,0.12)
- ylims: Tuple of ylims, if None will use (-0.12,0.12)
- zlims: Tuple of zlims, if None will use (-0.12,0.12)
- dpi: dpi to use when saving gif
- inetrval: Time to wait between frames
- legend: If True will add figure legend
- title: If True will add figure title