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