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

def get_image_positions( A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, res: tuple[int] = (200, 200)):
328def get_image_positions(A:Tensor,B:Tensor,C:Tensor, res:tuple[int]=(200,200)):
329    '''
330    Gets res[0] x res[1] points in the plane defined by ABC
331    :param A: Position of the top left corner of the image
332    :param B: Position of the top right corner of the image
333    :param C: Position of the bottom left corner of the image
334    :param res: Number of pixels as a tuple (X,Y). Default (200,200)
335    :returnns positions: The positions of pixels
336    '''
337    AB = torch.tensor([B[0] - A[0], B[1] - A[1], B[2] - A[2]]).to(device)
338    AC = torch.tensor([C[0] - A[0], C[1] - A[1], C[2] - A[2]]).to(device)
339
340    step_x = AB / res[0]
341    step_y = AC / res[1]
342
343    positions = torch.zeros((1,3,res[0]*res[1])).to(device)
344
345    for i in range(0,res[0]):
346        for j in range(res[1]):
347            positions[:,:,i*res[0]+j] = A + step_x * i + step_y * j
348        
349    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
def Visualise_single( A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, activation: torch.Tensor, colour_function: function = <function propagate_abs>, colour_function_args: dict = {}, res: tuple[int] = (200, 200), flip: bool = True) -> torch.Tensor:
352def Visualise_single(A:Tensor,B:Tensor,C:Tensor,activation:Tensor,
353                     colour_function:FunctionType=propagate_abs, colour_function_args:dict={}, 
354                     res:tuple[int]=(200,200), flip:bool=True) -> Tensor:
355    '''
356    Visalises field generated from activation to the plane ABC
357    :param A: Position of the top left corner of the image
358    :param B: Position of the top right corner of the image
359    :param C: Position of the bottom left corner of the image
360    :param activation: The transducer activation to use
361    :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`
362    :param colour_function_args: The arguments to pass to `colour_function`
363    :param res: Number of pixels as a tuple (X,Y). Default (200,200)
364    :param flip: Reverses X and Y directions. Default True
365    :return: Tensor of values of propagated field
366    '''
367    if len(activation.shape) < 3:
368        activation = activation.unsqueeze(0)
369    
370
371    
372    positions = get_image_positions(A,B,C,res)
373   
374    
375    # print(positions.shape)
376    # print(colour_function_args)
377    field_val = colour_function(activations=activation,points=positions,**colour_function_args)
378    if len(field_val.shape) < 3:
379        field_val.unsqueeze_(2)
380    results = []
381    for i in range(field_val.shape[2]):
382        result = torch.reshape(field_val[:,:,i], res)
383        results.append(result)
384    result = torch.stack(results,dim=2)
385
386    if flip:
387        result = torch.rot90(torch.fliplr(result))
388    
389    
390    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

def force_quiver( points: torch.Tensor, U: torch.Tensor, V: torch.Tensor, norm: tuple[int] = (0, 0, 0), ylims: int | None = None, xlims: int | None = None, log: bool = False, show: bool = True, colour: str | None = None, reciprocal: bool = False, block: bool = True, scale: float = 1) -> None:
392def force_quiver(points: Tensor, U:Tensor,V:Tensor,norm:tuple[int]=(0,0,0), ylims:int|None=None, xlims:int|None=None,
393                 log:bool=False,show:bool=True,colour:str|None=None, reciprocal:bool = False, block:bool=True, scale:float=1) -> None:
394    '''
395    Plot the force on a mesh as a quiver plot\n
396    :param points: The centre of the mesh faces
397    :param U: Force in first axis
398    :param V: Force in second axis
399    :param norm:
400    :param ylims: limit of y axis
401    :param zlims: limit of x axis
402    :param log: if `True` take the log of the values before plotting
403    :param show: if `True` call `plt.show()`
404    :param colour: colour of arrows
405    :param reciprocal: if `True` plot reciprocal of values
406    :param block: passed into `plt.show`
407    :param scale: value to scale arrows by - note will pass 1/scale to matplotlib
408    '''
409
410    B = points.shape[0]
411    N = points.shape[2]
412    
413    # if len(points) > 0:
414    #     pts_pos = get_point_pos(A,B,C,points,res)
415    
416    mask  = ~(torch.tensor(norm).to(bool))
417    points = points[:,mask,:]
418    # points=torch.reshape(points,(B,2,-1))
419    
420
421    xs = points[:,0,:].cpu().detach().numpy()[0]
422    ys = points[:,1,:].cpu().detach().numpy()[0]
423
424
425    if log:
426        U = torch.sign(U) * torch.abs(torch.log(torch.abs(U)))   
427        V = torch.sign(V) * torch.abs(torch.log(torch.abs(V))) 
428    
429    if reciprocal:
430        U = 1/U
431        V = 1/V
432    
433
434    plt.quiver(xs, ys, U.cpu().detach().numpy(),V.cpu().detach().numpy(),color = colour,linewidths=0.01,scale=1/scale)
435    plt.axis('equal')
436
437
438    if ylims is not None:
439        plt.ylim(ylims[0],ylims[1])
440    
441    if xlims is not None:
442        plt.xlim(xlims[0],xlims[1])
443    
444    if show:
445        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 True take the log of the values before plotting
  • show: if True call plt.show()
  • colour: colour of arrows
  • reciprocal: if True plot reciprocal of values
  • block: passed into plt.show
  • scale: value to scale arrows by - note will pass 1/scale to matplotlib
def force_quiver_3d( points: torch.Tensor, U: torch.Tensor, V: torch.Tensor, W: torch.Tensor, scale: float = 1, show: bool = True, ax: matplotlib.axes._axes.Axes | None = None) -> None:
449def force_quiver_3d(points:Tensor, U:Tensor,V:Tensor,W:Tensor, scale:float=1, show:bool=True, ax:mpl.axes.Axes|None=None) ->None:
450    '''
451    Plot the force on a mesh in 3D
452    :param points: The centre of the mesh faces
453    :param U: Force in first axis
454    :param V: Force in second axis
455    :param W: Force in third axis
456    :param scale: value to scale result by
457    :param show: If True will call `plt.show()`
458    :param ax: Axis to use 
459    '''
460    
461    if ax is None: ax = plt.figure().add_subplot(projection='3d')
462    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)
463    
464    if show: plt.show()
465    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
def Visualise_mesh( mesh: vedo.mesh.Mesh, colours: torch.Tensor | None = None, points: torch.Tensor | None = None, p_pressure: torch.Tensor | None = None, vmax: int | None = None, vmin: int | None = None, show: bool = True, subplot: int | matplotlib.axes._axes.Axes | None = None, fig: matplotlib.figure.Figure | None = None, buffer_x: int = 0, buffer_y: int = 0, buffer_z: int = 0, equalise_axis: bool = False, elev: float = -45, azim: float = 45, clamp: bool = False) -> None:
470def Visualise_mesh(mesh:Mesh, colours:Tensor|None=None, points:Tensor|None=None, p_pressure:Tensor|None=None,
471                   vmax:int|None=None,vmin:int|None=None, show:bool=True, subplot:int|plt.Axes|None=None, fig:plt.Figure|None=None, 
472                   buffer_x:int=0, buffer_y:int = 0, buffer_z:int = 0, equalise_axis:bool=False, elev:float=-45, azim:float=45, 
473                   clamp:bool=False) ->None:
474    '''
475    Plot a mesh in 3D and colour the mesh faces
476    :param mesh: Mesh to plot
477    :param colours: Colours for each face
478    :param points: Positions of points to also plot
479    :param p_pressure: Values to colour points with
480    :param vmax: Maximum colour to plot
481    :param vmin: Minimum colour to plot
482    :param show: If `True` call `plot.show()`
483    :param subplot: Optionally use existing subplot
484    :param fig: Optionally use existing fig
485    :param buffer_x: Amount of whitesapce to add in x direction
486    :param buffer_y: Amount of whitesapce to add in y direction
487    :param buffer_z: Amount of whitesapce to add in z direction
488    :param equalise_axis: If `True` call `ax.set_aspect('equal')`
489    :param elev: elevation angle
490    :param azim: azimuth angle
491    :param clamp: if True will clamp values in colours to vmax and vmin
492    '''
493
494    xmin,xmax, ymin,ymax, zmin,zmax = mesh.bounds()
495    
496    if type(colours) is torch.Tensor:
497        colours=colours.flatten()
498    
499    if clamp:
500        colours = torch.clamp(colours,vmin,vmax)
501
502
503    v = mesh.vertices
504    f = torch.tensor(mesh.cells)
505
506    if fig is None:
507        fig = plt.figure()
508    
509    if subplot is None:
510        ax = fig.add_subplot(projection="3d")
511    else:
512        ax = fig.add_subplot(subplot,projection="3d")
513
514    # norm = plt.Normalize(C.min(), C.max())
515    # colors = plt.cm.viridis(norm(C))
516
517    if vmin is None and colours is not None:
518        vmin = torch.min(colours).item()
519        if p_pressure is not None and p_pressure < vmin:
520            vmin = p_pressure
521    
522    if vmax is None and colours is not None:
523        vmax = torch.max(colours).item()
524        if p_pressure is not None and p_pressure > vmax:
525            vmax = p_pressure
526
527    norm = clrs.Normalize(vmin=vmin, vmax=vmax, clip=True)
528    mapper = cm.ScalarMappable(norm, cmap=cm.hot)
529
530    if points is not None:
531        if p_pressure is not None:
532            p_c = mapper.to_rgba(p_pressure.squeeze().cpu().detach())
533        else:
534            p_c = 'blue'
535        points = points.cpu().detach()
536        ax.scatter(points[:,0],points[:,1],points[:,2],color=p_c)
537
538    if colours is not None:
539        colour_mapped = []
540        for c in colours:
541            colour_mapped.append(mapper.to_rgba(c.cpu().detach()))
542    else:
543        colour_mapped=None
544
545    pc = art3d.Poly3DCollection(v[f], edgecolor="black", linewidth=0.01, facecolors=colour_mapped)
546    plt_3d = ax.add_collection(pc)
547
548
549    mappable = cm.ScalarMappable(cmap=cm.hot, norm=norm)
550    mappable.set_array(colour_mapped)  # Link the data to the ScalarMappable
551
552    # Add the color bar to the figure
553    cbar = fig.colorbar(mappable, ax=ax, shrink=0.6)
554    cbar.set_label('Face Value')
555
556
557    scale = mesh.vertices.flatten()
558    ax.auto_scale_xyz(scale, scale, scale)
559    
560    
561    if not equalise_axis:
562        ax.set_xlim([xmin - buffer_x, xmax +  buffer_x])
563        ax.set_ylim([ymin - buffer_y, ymax + buffer_y])
564        ax.set_zlim([zmin - buffer_z, zmax + buffer_z])
565    else:
566        ax.set_aspect('equal')
567
568
569    ax.view_init(elev=elev, azim=azim)
570
571
572
573    if show:
574        plt.show()
575    else:
576        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 True call plot.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 True call ax.set_aspect('equal')
  • elev: elevation angle
  • azim: azimuth angle
  • clamp: if True will clamp values in colours to vmax and vmin
def Visualise_line( A: torch.Tensor, B: torch.Tensor, x: torch.Tensor, F: torch.Tensor | None = None, points: torch.Tensor | None = None, steps: int = 1000, board: torch.Tensor | None = None, propagate_fun: function = <function propagate_abs>, propagate_args: dict = {}, show: bool = True) -> None:
581def Visualise_line(A:Tensor,B:Tensor,x:Tensor, F:Tensor|None=None,points:Tensor|None=None,steps:int = 1000, 
582                   board:Tensor|None=None, propagate_fun:FunctionType = propagate_abs, propagate_args:dict={}, show:bool=True) -> None:
583    '''
584    Plot the field across a line from A->B\n
585    :param A: Start of line
586    :param B: End of line
587    :param x: Hologram
588    :param F: Optionally, propagation matrix
589    :param points: Optionally, pass the points on line AB instead of computing them
590    :param steps: Number of points along line
591    :param board: Transducers to use
592    :param propagate_fun: Function to use to propagate hologram
593    :propagate_args: arguments for `propagate_fun`
594    :show: If `True` call `plt.show()`
595    '''
596    if board is None:
597        board = TRANSDUCERS
598    
599    if points is None:
600        AB = B-A
601        step = AB / steps
602        points = []
603        for i in range(steps):
604            p = A + i*step
605            points.append(p.unsqueeze(0))
606        
607        points = torch.stack(points, 2).to(device)
608    
609    
610    pressure = propagate_fun(activations=x,points=points, board=board,**propagate_args)
611    if show:
612        plt.plot(pressure.detach().cpu().flatten())
613        plt.show()
614    else:
615        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: If True call plt.show()
def ABC( size: float, plane: Literal['xz', 'yz', 'xy'] = 'xz', origin: torch.Tensor | tuple = None) -> tuple[torch.Tensor]:
618def ABC(size:float, plane:Literal['xz', 'yz', 'xy'] = 'xz', origin:Tensor|tuple=None) -> tuple[Tensor]:
619    '''
620    Get ABC values for visualisation
621    * A top right corner
622    * B top right corner
623    * C bottom left corner
624    :param size: The size of the window
625    :param plane: Plane, one of 'xz' 'yz' 'xy'
626    :param origin: The centre of the view window 
627    :return: A,B,C 
628    '''
629    if origin is None:
630        origin = torch.tensor((0,0,0), device=device)
631    if type(origin) == tuple or type(origin) == list:
632        origin = torch.tensor(origin).real
633    
634    origin = origin.squeeze().real
635
636    
637    if plane == 'xz':
638        A = origin+torch.tensor((-1,0, 1), device=device) * size
639        B = origin+torch.tensor((1,0, 1), device=device)* size
640        C = origin+torch.tensor((-1,0, -1), device=device)* size
641    
642    if plane == 'yz':
643        A = origin+torch.tensor((0,-1, 1), device=device) * size
644        B = origin+torch.tensor((0,1, 1), device=device)* size
645        C = origin+torch.tensor((0,-1, -1), device=device)* size
646    
647    if plane == 'xy':
648        A = origin+torch.tensor((-1,1, 0), device=device) * size
649        B = origin+torch.tensor((1, 1,0), device=device)* size
650        C = origin+torch.tensor((-1, -1,0), device=device)* size
651    
652    
653
654    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

def ABC_2_tiles(A: torch.Tensor, B: torch.Tensor, C: torch.Tensor):
656def ABC_2_tiles(A:Tensor,B:Tensor,C:Tensor):
657    '''
658    Split ABC defined region into 4 tiles
659    * A top right corner
660    * B top right corner
661    * C bottom left corner
662    
663    '''
664    A1 = A
665    B1 = A + (B-A)/2
666    C1 = A+ (C-A)/2
667
668    A2 = A+ (B-A)/2
669    B2 = B
670    C2 = A+ ((B-A)/2 + (C-A)/2)
671
672    A3 = A+ (C-A)/2
673    B3 = A + ((B-A)/2 + (C-A)/2)
674    C3 = C
675
676    A4 = A + ((B-A)/2 + (C-A)/2)
677    B4 = A + (B-A)+(C-A)/2
678    C4 = A + ((B-A)/2 + (C-A))
679
680    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
def combine_tiles( t1: torch.Tensor, t2: torch.Tensor, t3: torch.Tensor, t4: torch.Tensor):
682def combine_tiles(t1:Tensor,t2:Tensor,t3:Tensor,t4:Tensor):
683    '''
684    Combines subimages into a larger image, used in `Visualise_single_blocks`
685    :param t1: Top left image
686    :param t2: Top right image
687    :param t3: Bottom left image
688    :param t4: Bottom right image
689    '''
690    top = torch.cat([t1,t2],dim=1)
691    bottom = torch.cat([t3,t4],dim=1)
692
693    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
def Visualise_single_blocks( A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, activation: torch.Tensor, colour_function: function = <function propagate_abs>, colour_function_args: dict = {}, res: tuple[int] = (200, 200), flip: bool = True, depth=2) -> torch.Tensor:
695def Visualise_single_blocks(A:Tensor,B:Tensor,C:Tensor,activation:Tensor,
696                     colour_function:FunctionType=propagate_abs, colour_function_args:dict={}, 
697                     res:tuple[int]=(200,200), flip:bool=True, depth=2) -> Tensor:
698    '''
699    Visalises field generated from activation to the plane ABC in a slightly nicer memory efficient way by chunking into tiles
700    :param A: Position of the top left corner of the image
701    :param B: Position of the top right corner of the image
702    :param C: Position of the bottom left corner of the image
703    :param activation: The transducer activation to use
704    :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`
705    :param colour_function_args: The arguments to pass to `colour_function`
706    :param res: Number of pixels as a tuple (X,Y). Default (200,200)
707    :param flip: Reverses X and Y directions. Default True
708    :param depth: Number of times to chunk
709    :return: Tensor of values of propagated field
710    '''
711
712    tiles = ABC_2_tiles(A,B,C)
713
714    new_res = (int(res[0]/2), int(res[1]/2))
715
716    ims = []
717
718
719    for (nA,nB,nC) in tiles:
720        if depth == 1:
721            im = Visualise_single(nA,nB,nC,activation,colour_function=colour_function, colour_function_args=colour_function_args, res=new_res, flip=flip)
722        else:
723            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)
724        ims.append(im)
725        # torch.cuda.empty_cache()
726
727    im = combine_tiles(*ims)
728    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

def animate_lcode( pth, ax: matplotlib.axes._axes.Axes | None = None, fig: matplotlib.figure.Figure = None, skip: int = 1, show: bool = False, fname: str = '', extruder: torch.Tensor | None = None, xlims: tuple[float] | None = None, ylims: tuple[float] | None = None, zlims: tuple[float] | None = None, dpi: int = 100, interval: int = 1, legend: bool = True, title: bool = True) -> None:
731def animate_lcode(pth, ax:mpl.axes.Axes|None=None, fig:plt.Figure=None, skip:int=1, show:bool=False, 
732                  fname:str='', extruder:Tensor|None = None, xlims:tuple[float]|None=None, 
733                  ylims:tuple[float]|None=None, zlims:tuple[float]|None=None, dpi:int=100, interval:int = 1, 
734                  legend:bool=True, title:bool=True) -> None:
735    '''
736    Reads a .lcode file and produces a gif of the simulation of the result of that lcode\n
737    :param pth: Path to the .lcode file
738    :param ax: Axis to use, if None will create new
739    :param fig: Figure to use, if None will create new
740    :param skip: Number of instructions to skip per animation frame, default 1 (no skipping)
741    :param show: If true will call plt.show()
742    :param: fname: Name of file to save to
743    :param extruder: If not None the position of the extruder to plot as Tensor
744    :param xlims: Tuple of xlims, if None will use  (-0.12,0.12)
745    :param ylims: Tuple of ylims, if None will use  (-0.12,0.12)
746    :param zlims: Tuple of zlims, if None will use  (-0.12,0.12)
747    :param dpi: dpi to use when saving gif
748    :param inetrval: Time to wait between frames
749    :param legend: If True will add figure legend
750    :param title: If True will add figure title
751    '''
752
753    if fig is None: fig = plt.figure()
754    if ax is None: ax = fig.add_subplot(projection='3d')
755    
756
757    point_commands = ['L0','L1','L2','L3']
758
759    frames = []
760    printed_points = [[],]
761
762    functions = {}
763    in_function = None
764
765    
766
767    with open(pth,'r') as file:
768        lines = file.readlines()
769        LINES = len(lines)
770        for i,line in enumerate(lines):
771            print(f"Line {i}/{LINES}", end='\r')
772            line = line.replace(';','').rstrip()
773            split = line.split(':')
774            cmd = split[0]
775            
776            if cmd == 'function':
777                name = split[1]
778                functions[name] = []
779                in_function = name
780            
781            elif cmd == 'end':
782                name = split[1]
783                in_function = None
784            
785            elif cmd.startswith('F'):
786                frame_points = functions[cmd]
787                for frame in frame_points:
788                    frames.append(frame)
789                    frame_printed_points = printed_points[-1].copy()
790                    printed_points.append(frame_printed_points)
791
792
793
794            elif cmd in point_commands:
795                points = split[1:]
796                ps = []
797                for point in points:
798                    ps.append(point.split(','))
799
800                frame_points = [[float(p) for p in pt] for pt in ps]
801                frames.append(frame_points)
802
803                if in_function is not None:
804                    functions[in_function].append(frame_points)
805            
806            frame_printed_points = printed_points[-1].copy()
807            if cmd == 'C1':
808                frame_printed_points.append(frames[-1].copy())
809            
810            printed_points.append(frame_printed_points)
811
812
813    if extruder is not None:
814        if type(extruder) is bool:
815            extruder = create_points(1,1,0,-0.04, 0.04)
816        ex_x = extruder[:,0].detach().cpu()
817        ex_y = extruder[:,1].detach().cpu()
818        ex_z = extruder[:,2].detach().cpu()
819
820
821    FRAMES = int(len(frames) / skip)
822    # FRAMES = 100
823
824    if xlims is None:
825        xlims = (-0.12,0.12)
826    
827    if ylims is None:
828        ylims = (-0.12,0.12)
829    
830    if zlims is None:
831        zlims = (-0.12,0.12)
832    
833
834    def traverse(index):
835        index = index*skip
836        ax.clear()
837        print(f"Line {index/skip}/{FRAMES}", end='\r')
838        for pt in frames[index]:
839            ax.scatter(*pt, label='Trap')
840        
841        printed_xs = [i for i in [[p[0] for p in pt] for pt in printed_points[index]]]
842        printed_ys = [i for i in [[p[1] for p in pt] for pt in printed_points[index]]]
843        printed_zs = [i for i in [[p[2] for p in pt] for pt in printed_points[index]]]
844
845        ax.scatter(printed_xs,printed_ys, printed_zs, label='Printed', edgecolor='black')
846
847        ax.set_ylim(xlims)
848        ax.set_xlim(ylims)
849        ax.set_zlim(zlims)
850
851        
852        if extruder is not None: ax.scatter(ex_x, ex_y, ex_z, label='Extruder')
853
854        if legend: ax.legend()
855        if title: ax.set_title(f'Location: {index}')
856        
857
858
859    ani = animation.FuncAnimation(fig, traverse, frames=FRAMES, interval=interval)
860    if show: plt.show()
861
862    if fname == '':
863        fname = 'Results.gif'
864    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