src.acoustools.Mesh

  1from acoustools.Utilities import device, DTYPE, BOARD_POSITIONS
  2import acoustools.Constants as Constants
  3
  4import vedo, torch
  5import matplotlib.pyplot as plt
  6import numpy as np
  7
  8from torch import Tensor
  9from vedo import Mesh
 10from typing import Literal
 11
 12
 13def board_name(board:Tensor) -> str:
 14    '''
 15    Returns the name for a board, TOP and/or BOTTOM, used in cache system
 16    :param board: The board to use
 17    :return: name of board as `<'TOP'><'BOTTOM'><M>` for `M` transducers in the boards 
 18    '''
 19    M = board.shape[0]
 20
 21    top = "TOP" if 1 in torch.sign(board[:,2]) else ""
 22    bottom = "BOTTOM" if -1 in torch.sign(board[:,2]) else ""
 23    return top+bottom+str(M)
 24
 25def scatterer_file_name(scatterer:Mesh) ->str:
 26    '''
 27    Get a unique name to describe a scatterer position, calls `str(scatterer.coordinates)`
 28    ONLY USE TO SET FILENAME, USE `scatterer.filename` TO GET
 29    :param scatterer: The Mesh to use
 30    :return: Scatterer name
 31    
 32    '''
 33
 34    f_name = str(list(scatterer.coordinates)) + str(scatterer.cell_normals)
 35    return f_name
 36
 37def load_scatterer(path:str, compute_areas:bool = True, compute_normals:bool=True, dx:float=0,
 38                   dy:float=0,dz:float=0, rotx:float=0, roty:float=0, rotz:float=0, root_path:str="", force:bool=False, flip_normals=False) -> Mesh:
 39    '''
 40    Loads a scatterer as a `vedo` `Mesh` and applies translations as needed
 41    :param path: The name of the scatterer to load
 42    :param compute_areas: if `True` will call `scatterer.compute_cell_size()`. Default `True`
 43    :param compute_normals: if `True` will call `scatterer.compute_normals()`. Default `True`
 44    :param dx: Translation in the x direction to apply
 45    :param dy: Translation in the y direction to apply
 46    :param dz: Translation in the z direction to apply
 47    :param rotx: Rotation around the x axis to apply
 48    :param roty: Rotation around the y axis to apply
 49    :param rotz: Rotation around the z axis to apply
 50    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
 51    :return: The `vedo` `Mesh` of the scatterer
 52    '''
 53    scatterer = vedo.load(root_path+path, force=force)
 54    
 55    if scatterer is not None:
 56        if compute_areas: scatterer.compute_cell_size()
 57        if compute_normals: 
 58            scatterer.compute_normals()
 59            if flip_normals: scatterer.flip_normals()
 60
 61        scatterer.metadata["rotX"] = 0
 62        scatterer.metadata["rotY"] = 0
 63        scatterer.metadata["rotZ"] = 0
 64
 65        # scatterer.filename = scatterer.filename.split("/")[-1]
 66        scatterer.filename = scatterer_file_name(scatterer)
 67
 68        scatterer.metadata["FILE"] = scatterer.filename.split(".")[0]
 69
 70
 71        rotate(scatterer,(1,0,0),rotx)
 72        rotate(scatterer,(0,1,0),roty)
 73        rotate(scatterer,(0,0,1),rotz)
 74
 75        translate(scatterer,dx,dy,dz)
 76    else:
 77        raise ValueError(f"File not found at {path} - please check the path")
 78
 79    return scatterer
 80
 81def mesh_to_board(path:str, compute_areas:bool = True, compute_normals:bool=True, dx:float=0,
 82                   dy:float=0,dz:float=0, rotx:float=0, roty:float=0, rotz:float=0, root_path:str="", force:bool=False, flip_normals = True, diameter = 2*BOARD_POSITIONS, centre=True):
 83    '''
 84    Loads a scatterer as a `vedo` `Mesh` and interprets it as a transducer board with a transducer at each mesh centre
 85    :param path: The name of the scatterer to load
 86    :param compute_areas: if `True` will call `scatterer.compute_cell_size()`. Default `True`
 87    :param compute_normals: if `True` will call `scatterer.compute_normals()`. Default `True`
 88    :param dx: Translation in the x direction to apply
 89    :param dy: Translation in the y direction to apply
 90    :param dz: Translation in the z direction to apply
 91    :param rotx: Rotation around the x axis to apply
 92    :param roty: Rotation around the y axis to apply
 93    :param rotz: Rotation around the z axis to apply
 94    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
 95    :param flip_normals: If True will flip the normals 
 96    :param diameter: Size to scale the mesh to in thr x-axis
 97    :return: The `vedo` `Mesh` of the scatterer
 98    '''
 99    
100    scatterer = load_scatterer(path=path,compute_areas=compute_areas,  compute_normals=compute_normals, 
101                               dx=dx, dy=dy, dz=dz, rotx=rotx, roty=roty, rotz=rotz, root_path=root_path,force=force)
102    
103    if centre: centre_scatterer(scatterer)
104    
105    if diameter is not None: scale_to_diameter(scatterer, diameter)
106    
107    centres = get_centres_as_points(scatterer).squeeze(0).permute(1,0)
108    norms = get_normals_as_points(scatterer).squeeze(0).permute(1,0)  
109    if flip_normals: norms = norms * -1
110
111    return centres, norms
112    
113
114def calculate_features(scatterer:Mesh, compute_areas:bool = True, compute_normals:bool=True):
115    '''
116    @private
117    '''
118    if compute_areas: scatterer.compute_cell_size()
119    if compute_normals: scatterer.compute_normals()
120
121    scatterer.filename = scatterer_file_name(scatterer)
122    scatterer.metadata["FILE"] = scatterer.filename.split(".")[0]
123
124
125def load_multiple_scatterers(paths:list[str],  compute_areas:bool = True, compute_normals:bool=True, 
126                             dxs:list[int]=[],dys:list[int]=[],dzs:list[int]=[], rotxs:list[int]=[], rotys:list[int]=[], rotzs:list[int]=[], root_path:str="") -> Mesh:
127    '''
128    Loads multiple scatterers and combines them into a single scatterer object
129    :param path: The name of the scatterers to load
130    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default True
131    :param compute_normals: if true will call `scatterer.compute_normals()`. Default True
132    :param dxs: List of translations in the x direction to apply to each scatterer
133    :param dys: List of translations in the y direction to apply to each scatterer
134    :param dzs: List of translations in the z direction to apply to each scatterer
135    :param rotxs: List pf rotations around the x axis to apply to each scatterer
136    :param rotys: List pf rotations around the y axis to apply to each scatterer
137    :param rotzs: List pf rotations around the z axis to apply to each scatterer
138    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
139    :return: A merged mesh from all of the paths provided
140    '''
141    dxs += [0] * (len(paths) - len(dxs))
142    dys += [0] * (len(paths) - len(dys))
143    dzs += [0] * (len(paths) - len(dzs))
144
145    rotxs += [0] * (len(paths) - len(rotxs))
146    rotys += [0] * (len(paths) - len(rotys))
147    rotzs += [0] * (len(paths) - len(rotzs))
148
149    scatterers = []
150    for i,path in enumerate(paths):
151        scatterer = load_scatterer(path, compute_areas, compute_normals, dxs[i],dys[i],dzs[i],rotxs[i],rotys[i],rotzs[i],root_path)
152        scatterers.append(scatterer)
153    combined = merge_scatterers(*scatterers)
154    return combined
155
156def merge_scatterers(*scatterers:Mesh, flag:bool=False) ->Mesh:
157    '''
158    Combines any number of scatterers into a single scatterer\n
159    :param scatterers: any number of scatterers to combine
160    :param flag: Value will be passed to `vedo.merge`
161    :return: the combined scatterer
162    '''
163    names = []
164    Fnames = []
165    for scatterer in scatterers:
166        names.append(scatterer_file_name(scatterer))
167        Fnames.append(scatterer.metadata["FILE"][0])
168    
169    if flag:
170        combined = vedo.merge(scatterers, flag=True)
171    else:
172        combined = vedo.merge(scatterers)
173    combined.filename = "".join(names)
174    combined.metadata["FILE"] = "".join(Fnames)
175    return combined
176
177
178def scale_to_diameter(scatterer:Mesh , diameter: float, reset:bool=True, origin:bool=True) -> None:
179    '''
180    Scale a mesh to a given diameter in the x-axis and recomputes normals and areas \n
181    Modifies scatterer in place so does not return anything.\n
182
183    :param scatterer: The scatterer to scale
184    :param diameter: The diameter target
185    '''
186    x1,x2,y1,y2,z1,z2 = scatterer.bounds()
187    diameter_sphere = x2 - x1
188    scatterer.scale(diameter/diameter_sphere,reset=reset, origin=origin)
189    scatterer.compute_cell_size()
190    scatterer.compute_normals()
191    scatterer.filename = scatterer_file_name(scatterer)
192    
193def get_diameter(scatterer:Mesh):
194    x1,x2,y1,y2,z1,z2 = scatterer.bounds()
195    diameter_sphere = torch.norm(torch.Tensor([x2,]) - torch.Tensor([x1,]), p=2)
196    return diameter_sphere
197
198
199def get_plane(scatterer: Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> Mesh:
200    '''
201    Get intersection of a scatterer and a plane\n
202    :param scatterer: The scatterer to intersect
203    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
204    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
205    :return: new `Mesh` Containing the intersection of the plane and the scatterer
206    '''
207    intersection = scatterer.clone().intersect_with_plane(origin,normal)
208    intersection.filename = scatterer.filename + "plane" + str(origin)+str(normal)
209    return intersection
210
211def get_lines_from_plane(scatterer:Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> list[int]:
212    '''
213    Gets the edges on a plane from the intersection between a scatterer and the plane\n
214    :param scatterer: The scatterer to intersect
215    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
216    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
217    :return: a list of edges in the plane 
218    '''
219
220    mask = [0,0,0]
221    for i in range(3):
222        mask[i] =not normal[i]
223    mask = np.array(mask)
224
225    intersection = get_plane(scatterer, origin, normal)
226    verticies = intersection.vertices
227    lines = intersection.lines
228
229    connections = []
230
231    for i in range(len(lines)):
232        connections.append([verticies[lines[i][0]][mask],verticies[lines[i][1]][mask]])
233
234    return connections
235
236def plot_plane(connections:list[int]) -> None:
237    '''
238    Plot a set of edges assuming they are co-planar\n
239    :param connections: list of connections to plot
240    '''
241    
242    for con in connections:
243        xs = [con[0][0], con[1][0]]
244        ys = [con[0][1], con[1][1]]
245        plt.plot(xs,ys,color = "blue")
246
247    plt.xlim((-0.06,0.06))
248    plt.ylim((-0.06,0.06))
249    plt.show()
250
251def get_normals_as_points(*scatterers:Mesh, permute_to_points:bool=True) -> Tensor:
252    '''
253    Returns the normal vectors to the surface of a scatterer as a `torch` `Tensor` as acoustools points\n
254    :param scatterers: The scatterer to use
255    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
256    :return: normals
257    '''
258    norm_list = []
259    for scatterer in scatterers:
260        scatterer.compute_normals()
261        norm =  torch.tensor(scatterer.cell_normals).to(device)
262
263        if permute_to_points:
264            norm = torch.permute(norm,(1,0))
265        
266        norm_list.append(norm.to(DTYPE))
267    
268    return torch.stack(norm_list)
269
270def get_centre_of_mass_as_points(*scatterers:Mesh, permute_to_points:bool=True) ->Tensor:
271    '''
272    Returns the centre of mass(es) of a scatterer(s) as a `torch` `Tensor` as acoustools points\n
273    :param scatterers: The scatterer(s) to use
274    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
275    :return: centre of mass(es)
276    '''
277    centres_list = []
278    for scatterer in scatterers:
279        centre_of_mass =  torch.tensor(scatterer.center_of_mass()).to(DTYPE).to(device)
280
281        if permute_to_points:
282            centre_of_mass = torch.unsqueeze(centre_of_mass,1)
283        
284        centres_list.append(centre_of_mass.to(DTYPE))
285    
286    return torch.real(torch.stack(centres_list))
287
288
289def get_centres_as_points(*scatterers:Mesh, permute_to_points:bool=True, add_normals:bool=False, normal_scale:float=0.001) ->Tensor:
290    '''
291    Returns the centre of scatterer faces as a `torch` `Tensor` as acoustools points\n
292    :param scatterers: The scatterer to use
293    :param permute_to_points: If `True` will permute the order of coordinates to agree with what acoustools expects.
294    :return: centres
295    '''
296    centre_list = []
297    for scatterer in scatterers:
298        centres =  torch.tensor(scatterer.cell_centers().points).to(DTYPE).to(device)
299
300        if permute_to_points:
301            centres = torch.permute(centres,(1,0)).unsqueeze_(0)
302        
303        if add_normals:
304            norms= get_normals_as_points(scatterer)
305            centres += norms.real * normal_scale
306        
307        centre_list.append(centres)
308    centres = torch.cat(centre_list,dim=0)
309    return centres
310
311def get_verticies_as_points(*scatterers:Mesh):
312    '''
313    Gets the verticies of a mesh as a Tensor of AcousTools (B,3,N) points \n
314    :param Mesh: Mesh to use
315    :returns verticies: verticies as points
316    '''
317   
318    vert_list = []
319    for scatterer in scatterers:
320        vert =  torch.tensor(scatterer.vertices).to(DTYPE).to(device)
321        vert_list.append(vert)
322
323    verts = torch.cat(vert_list,dim=0).unsqueeze(0).permute(0,2,1)
324    return verts
325
326def get_cell_verticies(*scatterers:Mesh):
327    '''
328    Gets a tensor of (B,3,M,3) - batch x (xyz) x Faces x (vertex) \n
329    :param Mesh: Mesh to use
330    :returns verticies: verticies
331    '''
332    verts = get_verticies_as_points(*scatterers)
333    vert_list = []
334    for scatterer in scatterers:
335        cells = torch.tensor(scatterer.cells)
336        N = cells.shape[0]
337        cell_indexes = cells.flatten()
338        cell_verts = torch.index_select(verts, 2, cell_indexes)
339        cell_verts=cell_verts.reshape(1,3,N,3)
340
341
342        vert_list.append(cell_verts)
343    verts = torch.cat(vert_list,dim=0)
344    return verts
345
346
347def get_barycentric_points(*scatterers:Mesh, N=7, sum=True):
348    '''
349    @private
350    '''
351    
352
353    if N != 7: raise ValueError("Only N=7 is supported") #Allow for N as a parameter incase it it implemented in future
354
355    cell_verts = get_cell_verticies(*scatterers)
356
357    DUNAVANT_7 = torch.tensor([
358    [1/3, 1/3, 1/3, 0.225],
359    [0.0597158717, 0.4701420641, 0.4701420641, 0.1323941527],
360    [0.4701420641, 0.0597158717, 0.4701420641, 0.1323941527],
361    [0.4701420641, 0.4701420641, 0.0597158717, 0.1323941527],
362    [0.7974269853, 0.1012865073, 0.1012865073, 0.1259391805],
363    [0.1012865073, 0.7974269853, 0.1012865073, 0.1259391805],
364    [0.1012865073, 0.1012865073, 0.7974269853, 0.1259391805],
365    ])
366    DUNAVANT_7_abg = DUNAVANT_7[:,:3].permute(1,0).unsqueeze(0).unsqueeze(0).unsqueeze(0)
367
368
369    DUNAVANT_7_W = DUNAVANT_7[:,3]
370
371    cell_verts = cell_verts.unsqueeze(-1)
372    barycentric_verts = cell_verts * DUNAVANT_7_abg
373    if sum: barycentric_verts = barycentric_verts.sum(dim=3)
374
375    return barycentric_verts, DUNAVANT_7_W
376
377    
378
379def get_areas(*scatterers: Mesh) -> Tensor:
380    '''
381    Returns the areas of faces of any number of scatterers\n
382    :param scatterers: The scatterers to use.
383    :return: areas
384    '''
385    area_list = []
386    for scatterer in scatterers:
387        scatterer.compute_cell_size()
388        area_list.append(torch.Tensor(scatterer.celldata["Area"]).to(device))
389    
390    return torch.stack(area_list)
391
392def get_weight(scatterer:Mesh, density:float=Constants.p_p, g:float=9.81) -> float:
393    '''
394    Get the weight of a scatterer\\
395    :param scatterer: The scatterer to use\\
396    :param density: The density to use. Default density for EPS\\
397    :param g: value for g to use. Default 9.81\\
398    :return: weight
399    '''
400    mass = scatterer.volume() * density
401    return g * mass
402
403def translate(scatterer:Mesh, dx:float=0,dy:float=0,dz:float=0) -> None:
404    '''
405    Translates a scatterer by (dx,dy,dz) \n
406    Modifies inplace so does not return a value \n
407    :param scatterer: The scatterer to use
408    :param dx: Translation in the x direction
409    :param dy: Translation in the y direction
410    :param dz: Translation in the z direction
411    '''
412    scatterer.shift(np.array([dx,dy,dz]))
413    scatterer.filename = scatterer_file_name(scatterer)
414
415def rotate(scatterer:Mesh, axis:tuple[int], rot:float, centre:tuple[int]=(0, 0, 0), rotate_around_COM:bool=False):
416    '''
417    Rotates a scatterer in axis by rot\n
418    Modifies inplace so does not return a value\n
419    :param scatterer: The scatterer to use
420    :param axis: The axis to rotate in
421    :param rot: Angle to rotate in degrees
422    :param centre: point to rotate around
423    :param rotate_around_COM: If True will set `centre` to `scatterer`s centre of mass
424    '''
425    if rotate_around_COM:
426        centre = vedo.vector(get_centre_of_mass_as_points(scatterer).cpu().detach().squeeze())
427
428    if axis[0]:
429        scatterer.metadata["rotX"] = scatterer.metadata["rotX"] + rot
430    if axis[1]:
431        scatterer.metadata["rotY"] = scatterer.metadata["rotY"] + rot
432    if axis[2]:
433        scatterer.metadata["rotZ"] = scatterer.metadata["rotZ"] + rot
434    scatterer.rotate(rot, axis,point=centre)
435    scatterer.filename = scatterer_file_name(scatterer)
436
437 
438def downsample(scatterer:Mesh, factor:int=2, n:int|None=None, method:str='quadric', boundaries:bool=False, compute_areas:bool=True, compute_normals:bool=True) -> Mesh:
439    '''
440    Downsamples a mesh to have `factor` less elements\n
441    :param scatterer: The scatterer to use
442    :param factor: The factor to downsample by
443    :param n: The desired number of final points, passed to `Vedo.Mesh.decimate`
444    :param method:, `boundaries` - passed to `vedo.decimate`
445    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default `True`
446    :param compute_normals: if true will call `scatterer.compute_normals()`. Default `True`
447    :return: downsampled mesh
448    '''
449    scatterer_small =  scatterer.decimate(1/factor, n, method, boundaries)
450    
451    scatterer_small.metadata["rotX"] = scatterer.metadata["rotX"]
452    scatterer_small.metadata["rotY"] = scatterer.metadata["rotY"]
453    scatterer_small.metadata["rotZ"] = scatterer.metadata["rotZ"]
454
455    if compute_areas: scatterer_small.compute_cell_size()
456    if compute_normals: 
457        scatterer_small.compute_normals()
458
459    scatterer_small.filename = scatterer_file_name(scatterer_small)  + "-scale-" + str(factor)
460
461
462    return scatterer_small
463
464
465def centre_scatterer(scatterer:Mesh) -> list[int]:
466    '''
467    Translate scatterer so the centre of mass is at (0,0,0)\n
468    Modifies Mesh in place \n
469    :param scatterer: Scatterer to centre
470    :return: Returns the amount needed to move in each direction
471    '''
472    com = get_centre_of_mass_as_points(scatterer).cpu()
473    correction = [-1*com[:,0].item(), -1*com[:,1].item(), -1*com[:,2].item()]
474    translate(scatterer, dx = correction[0], dy = correction[1], dz=  correction[2])
475
476    return correction
477
478
479def get_edge_data(scatterer:Mesh, wavelength:float=Constants.wavelength, print_output:bool=True, break_down_average:bool=False) -> None|tuple[float]:
480    '''
481    Get the maximum, minimum and average size of edges in a mesh. Optionally prints or returns the result.\n
482    :param scatterer: Mesh of interest
483    :param wavelength: Wavenelgth size for printing results as multiple of some wavelength
484    :param print_output: If True, prints results else returns values
485    :break_down_average: If True will also return (distance_sum, N)
486    :return: None if `print_outputs` is `True` else returns `(max_distance, min_distance, average_distance)` and optionally  (distance_sum, N)
487
488    '''
489    points = scatterer.vertices
490
491    distance_sum = 0
492    N = 0
493
494    max_distance = 0
495    min_distance = 100000000
496
497
498    for (start,end) in scatterer.edges:
499        start_point = points[start]
500        end_point = points[end]
501        sqvec = torch.Tensor((start_point-end_point)**2)
502        # print(sqvec, torch.sum(sqvec)**0.5)
503        distance = torch.sum(sqvec)**0.5
504        distance_sum += distance
505        N += 1
506        if distance < min_distance:
507            min_distance = distance
508        if distance > max_distance:
509            max_distance = distance
510
511    average_distance = distance_sum/N
512
513    if print_output:
514        print('Max Distance', max_distance.item(),'=' ,max_distance.item()/wavelength, 'lambda')
515        print('Min Distance', min_distance.item(),'=', min_distance.item()/wavelength, 'lambda')
516        print('Ave Distance', average_distance.item(),'=', average_distance.item()/wavelength, 'lambda')
517    else:
518        if break_down_average:
519            return (max_distance, min_distance, average_distance), (distance_sum, N)
520        else:
521            return (max_distance, min_distance, average_distance)
522
523
524def cut_mesh_to_walls(scatterer:Mesh, layer_z:float, layer_normal:tuple[float] = (0,0,-1.0), wall_thickness = 0.001) -> Mesh:
525    '''
526    Cuts a mesh with a given plane and then converts the result to have walls of a certain thickness \n
527    :param scatterer: Mesh to use
528    :param layer_z: coordinate of layer
529    :param layer_normal: Normal to layer (if not +- (0,0,1) then layer_z will not refer to a z coordinate)
530    :param wall_thickness: Thickness of the walls to returns
531    :return: Cut mesh with walls
532    '''
533
534    xmin,xmax, ymin,ymax, zmin,zmax = scatterer.bounds()
535    dx = xmax-xmin
536    dy = ymax-ymin
537
538    scale_x = (dx-2*wall_thickness) / dx
539    scale_y = (dy-2*wall_thickness) / dy
540
541    outler_layer = scatterer.cut_with_plane((0,0,layer_z),layer_normal)
542    inner_layer = outler_layer.clone()
543    inner_layer.scale((scale_x,scale_y,1), origin=False)
544
545    com_outer = get_centre_of_mass_as_points(outler_layer)
546    com_inner = get_centre_of_mass_as_points(inner_layer)
547
548    d_com = (com_outer - com_inner).squeeze()
549
550    translate(inner_layer, *d_com)
551
552    walls = vedo.merge(outler_layer, inner_layer)
553
554
555    boundaries_outer = outler_layer.boundaries()
556    boundaries_inner = inner_layer.boundaries()
557
558    strips = boundaries_outer.join_with_strips(boundaries_inner).triangulate()
559    
560
561    walls = vedo.merge(walls,strips)
562    
563    calculate_features(walls)
564    scatterer_file_name(walls)
565
566    return walls.clean()
567
568def cut_closed_scatterer(scatterer:Mesh,layer_z:float, normals=[(0,0,1)]):
569    '''
570    Cuts a scatterer across a z-plane\\
571    :param scatterer: Mesh
572    :param layer_z: height to cute
573    :param normals: Which way is up 
574    '''
575    origins=[(0,0,layer_z)]
576    closed_scatterer = scatterer.cut_closed_surface(origins=origins, normals=normals)
577    return closed_scatterer
578
579def get_volume(scatterer:Mesh):
580    '''
581    Returns the volume of a mesh
582    '''
583    return scatterer.volume()
584
585def insert_parasite(scatterer:Mesh, parasite_path:str = '/Sphere-lam1.stl', root_path:str="../BEMMedia", parasite_size:float=Constants.wavelength/4, parasite_offset:Tensor=None) -> Mesh:
586    '''
587    Inserts a parasitic body into an existing scatterer. Used to supress the resonance from BEM \n
588    See https://doi.org/10.1109/8.310000 \n
589    :param scatterer: The scatterer to insert parasite into
590    :param parasite_path: The path to the mesh to load and use as parasite
591    :param root_path: The folder to load the file from
592    :param parasite_size: The diameter to scale the parasite to
593    :param parasite_offset: Tensor of offsets for the parasite from the (0,0,0) point
594    :returns: Scatterer with parasite inserted
595    '''
596    parasite = load_scatterer(parasite_path, root_path=root_path)
597    centre_scatterer(parasite)
598    if parasite_offset is None:
599        parasite_offset = get_centre_of_mass_as_points(scatterer)
600
601    dx = parasite_offset[:,0].item()
602    dy = parasite_offset[:,1].item()
603    dz = parasite_offset[:,2].item()
604
605    translate(parasite, dx=dx, dy=dy, dz=dz)
606
607    scale_to_diameter(parasite, parasite_size)
608
609    infected_scatterer = merge_scatterers(scatterer, parasite)
610
611    return infected_scatterer
612
613def get_CHIEF_points(scatterer:Mesh, P=30, method:Literal['random', 'uniform', 'volume-random']='random', start:Literal['surface', 'centre']='surface', scale=0.001, scale_mode:Literal['abs','diameter-scale']='abs') -> Mesh:
614    '''
615    Generates internal points that can be used for the CHIEF BEM formulation (or any other reason)\n
616    :param scatterer: The scatterer to insert points into
617    :param P: Number of points. if P=-1 then P= number of mesh elements
618    :param method: The method used to generate points \n
619        - random: will move scale metres along each of P randomly selected normals \n
620        - uniform:  will move scale metres along each of P uniformly spaced normals (based on order coming from `Mesh.get_normals_as_points`) \n
621        - volume-random: will use `vedo.Mesh..generate_random_points` to generate P internal points
622    :param start: The point to use as the basis for generating points \n
623         - surface: Will step along normals from surface (will step in the -ve normal direction)
624         - centre: Will step along normal from centre of mass (will step in +ve normal direction)
625    :param scale: The distance in m to step 
626    :returns internal points:
627    '''
628
629    centre_norms = get_normals_as_points(scatterer, permute_to_points=False)
630
631    if scale_mode.lower() == 'diameter-scale':
632        d = get_diameter(scatterer)
633        scale = scale * d
634    
635
636    if start.lower() == 'centre':
637        centres = get_centre_of_mass_as_points(scatterer, permute_to_points=False).unsqueeze(1)
638        internal_points = centres + centre_norms * scale      
639
640    else:
641        centres = torch.tensor(scatterer.cell_centers().points, dtype=DTYPE, device=device)
642        internal_points = centres - centre_norms * scale
643
644    M = centre_norms.shape[1]
645    
646    if P == -1: P = M
647
648   
649
650
651    
652    if method.lower() == 'random':
653        indices = torch.randperm(M)[:P]
654        internal_points = internal_points[:, indices,:]
655
656    elif method.lower()== 'uniform':
657        idx = [i for i in range(M) if i%(int(M/P)) == 0]
658        internal_points = internal_points[:, idx,:]
659    elif method.lower() == 'volume-random':
660        internal_points = torch.Tensor(scatterer.generate_random_points(P).points).unsqueeze(0)
661
662    internal_points = internal_points.permute(0,2,1)
663
664
665    return internal_points
def board_name(board: torch.Tensor) -> str:
14def board_name(board:Tensor) -> str:
15    '''
16    Returns the name for a board, TOP and/or BOTTOM, used in cache system
17    :param board: The board to use
18    :return: name of board as `<'TOP'><'BOTTOM'><M>` for `M` transducers in the boards 
19    '''
20    M = board.shape[0]
21
22    top = "TOP" if 1 in torch.sign(board[:,2]) else ""
23    bottom = "BOTTOM" if -1 in torch.sign(board[:,2]) else ""
24    return top+bottom+str(M)

Returns the name for a board, TOP and/or BOTTOM, used in cache system

Parameters
  • board: The board to use
Returns

name of board as <'TOP'><'BOTTOM'><M> for M transducers in the boards

def scatterer_file_name(scatterer: vedo.mesh.Mesh) -> str:
26def scatterer_file_name(scatterer:Mesh) ->str:
27    '''
28    Get a unique name to describe a scatterer position, calls `str(scatterer.coordinates)`
29    ONLY USE TO SET FILENAME, USE `scatterer.filename` TO GET
30    :param scatterer: The Mesh to use
31    :return: Scatterer name
32    
33    '''
34
35    f_name = str(list(scatterer.coordinates)) + str(scatterer.cell_normals)
36    return f_name

Get a unique name to describe a scatterer position, calls str(scatterer.coordinates) ONLY USE TO SET FILENAME, USE scatterer.filename TO GET

Parameters
  • scatterer: The Mesh to use
Returns

Scatterer name

def load_scatterer( path: str, compute_areas: bool = True, compute_normals: bool = True, dx: float = 0, dy: float = 0, dz: float = 0, rotx: float = 0, roty: float = 0, rotz: float = 0, root_path: str = '', force: bool = False, flip_normals=False) -> vedo.mesh.Mesh:
38def load_scatterer(path:str, compute_areas:bool = True, compute_normals:bool=True, dx:float=0,
39                   dy:float=0,dz:float=0, rotx:float=0, roty:float=0, rotz:float=0, root_path:str="", force:bool=False, flip_normals=False) -> Mesh:
40    '''
41    Loads a scatterer as a `vedo` `Mesh` and applies translations as needed
42    :param path: The name of the scatterer to load
43    :param compute_areas: if `True` will call `scatterer.compute_cell_size()`. Default `True`
44    :param compute_normals: if `True` will call `scatterer.compute_normals()`. Default `True`
45    :param dx: Translation in the x direction to apply
46    :param dy: Translation in the y direction to apply
47    :param dz: Translation in the z direction to apply
48    :param rotx: Rotation around the x axis to apply
49    :param roty: Rotation around the y axis to apply
50    :param rotz: Rotation around the z axis to apply
51    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
52    :return: The `vedo` `Mesh` of the scatterer
53    '''
54    scatterer = vedo.load(root_path+path, force=force)
55    
56    if scatterer is not None:
57        if compute_areas: scatterer.compute_cell_size()
58        if compute_normals: 
59            scatterer.compute_normals()
60            if flip_normals: scatterer.flip_normals()
61
62        scatterer.metadata["rotX"] = 0
63        scatterer.metadata["rotY"] = 0
64        scatterer.metadata["rotZ"] = 0
65
66        # scatterer.filename = scatterer.filename.split("/")[-1]
67        scatterer.filename = scatterer_file_name(scatterer)
68
69        scatterer.metadata["FILE"] = scatterer.filename.split(".")[0]
70
71
72        rotate(scatterer,(1,0,0),rotx)
73        rotate(scatterer,(0,1,0),roty)
74        rotate(scatterer,(0,0,1),rotz)
75
76        translate(scatterer,dx,dy,dz)
77    else:
78        raise ValueError(f"File not found at {path} - please check the path")
79
80    return scatterer

Loads a scatterer as a vedo Mesh and applies translations as needed

Parameters
  • path: The name of the scatterer to load
  • compute_areas: if True will call scatterer.compute_cell_size(). Default True
  • compute_normals: if True will call scatterer.compute_normals(). Default True
  • dx: Translation in the x direction to apply
  • dy: Translation in the y direction to apply
  • dz: Translation in the z direction to apply
  • rotx: Rotation around the x axis to apply
  • roty: Rotation around the y axis to apply
  • rotz: Rotation around the z axis to apply
  • root_path: The folder containing the file, the scatterer to be loaded will be loaded from root_path+path
Returns

The vedo Mesh of the scatterer

def mesh_to_board( path: str, compute_areas: bool = True, compute_normals: bool = True, dx: float = 0, dy: float = 0, dz: float = 0, rotx: float = 0, roty: float = 0, rotz: float = 0, root_path: str = '', force: bool = False, flip_normals=True, diameter=0.2365, centre=True):
 82def mesh_to_board(path:str, compute_areas:bool = True, compute_normals:bool=True, dx:float=0,
 83                   dy:float=0,dz:float=0, rotx:float=0, roty:float=0, rotz:float=0, root_path:str="", force:bool=False, flip_normals = True, diameter = 2*BOARD_POSITIONS, centre=True):
 84    '''
 85    Loads a scatterer as a `vedo` `Mesh` and interprets it as a transducer board with a transducer at each mesh centre
 86    :param path: The name of the scatterer to load
 87    :param compute_areas: if `True` will call `scatterer.compute_cell_size()`. Default `True`
 88    :param compute_normals: if `True` will call `scatterer.compute_normals()`. Default `True`
 89    :param dx: Translation in the x direction to apply
 90    :param dy: Translation in the y direction to apply
 91    :param dz: Translation in the z direction to apply
 92    :param rotx: Rotation around the x axis to apply
 93    :param roty: Rotation around the y axis to apply
 94    :param rotz: Rotation around the z axis to apply
 95    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
 96    :param flip_normals: If True will flip the normals 
 97    :param diameter: Size to scale the mesh to in thr x-axis
 98    :return: The `vedo` `Mesh` of the scatterer
 99    '''
100    
101    scatterer = load_scatterer(path=path,compute_areas=compute_areas,  compute_normals=compute_normals, 
102                               dx=dx, dy=dy, dz=dz, rotx=rotx, roty=roty, rotz=rotz, root_path=root_path,force=force)
103    
104    if centre: centre_scatterer(scatterer)
105    
106    if diameter is not None: scale_to_diameter(scatterer, diameter)
107    
108    centres = get_centres_as_points(scatterer).squeeze(0).permute(1,0)
109    norms = get_normals_as_points(scatterer).squeeze(0).permute(1,0)  
110    if flip_normals: norms = norms * -1
111
112    return centres, norms

Loads a scatterer as a vedo Mesh and interprets it as a transducer board with a transducer at each mesh centre

Parameters
  • path: The name of the scatterer to load
  • compute_areas: if True will call scatterer.compute_cell_size(). Default True
  • compute_normals: if True will call scatterer.compute_normals(). Default True
  • dx: Translation in the x direction to apply
  • dy: Translation in the y direction to apply
  • dz: Translation in the z direction to apply
  • rotx: Rotation around the x axis to apply
  • roty: Rotation around the y axis to apply
  • rotz: Rotation around the z axis to apply
  • root_path: The folder containing the file, the scatterer to be loaded will be loaded from root_path+path
  • flip_normals: If True will flip the normals
  • diameter: Size to scale the mesh to in thr x-axis
Returns

The vedo Mesh of the scatterer

def load_multiple_scatterers( paths: list[str], compute_areas: bool = True, compute_normals: bool = True, dxs: list[int] = [], dys: list[int] = [], dzs: list[int] = [], rotxs: list[int] = [], rotys: list[int] = [], rotzs: list[int] = [], root_path: str = '') -> vedo.mesh.Mesh:
126def load_multiple_scatterers(paths:list[str],  compute_areas:bool = True, compute_normals:bool=True, 
127                             dxs:list[int]=[],dys:list[int]=[],dzs:list[int]=[], rotxs:list[int]=[], rotys:list[int]=[], rotzs:list[int]=[], root_path:str="") -> Mesh:
128    '''
129    Loads multiple scatterers and combines them into a single scatterer object
130    :param path: The name of the scatterers to load
131    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default True
132    :param compute_normals: if true will call `scatterer.compute_normals()`. Default True
133    :param dxs: List of translations in the x direction to apply to each scatterer
134    :param dys: List of translations in the y direction to apply to each scatterer
135    :param dzs: List of translations in the z direction to apply to each scatterer
136    :param rotxs: List pf rotations around the x axis to apply to each scatterer
137    :param rotys: List pf rotations around the y axis to apply to each scatterer
138    :param rotzs: List pf rotations around the z axis to apply to each scatterer
139    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
140    :return: A merged mesh from all of the paths provided
141    '''
142    dxs += [0] * (len(paths) - len(dxs))
143    dys += [0] * (len(paths) - len(dys))
144    dzs += [0] * (len(paths) - len(dzs))
145
146    rotxs += [0] * (len(paths) - len(rotxs))
147    rotys += [0] * (len(paths) - len(rotys))
148    rotzs += [0] * (len(paths) - len(rotzs))
149
150    scatterers = []
151    for i,path in enumerate(paths):
152        scatterer = load_scatterer(path, compute_areas, compute_normals, dxs[i],dys[i],dzs[i],rotxs[i],rotys[i],rotzs[i],root_path)
153        scatterers.append(scatterer)
154    combined = merge_scatterers(*scatterers)
155    return combined

Loads multiple scatterers and combines them into a single scatterer object

Parameters
  • path: The name of the scatterers to load
  • compute_areas: if true will call scatterer.compute_cell_size(). Default True
  • compute_normals: if true will call scatterer.compute_normals(). Default True
  • dxs: List of translations in the x direction to apply to each scatterer
  • dys: List of translations in the y direction to apply to each scatterer
  • dzs: List of translations in the z direction to apply to each scatterer
  • rotxs: List pf rotations around the x axis to apply to each scatterer
  • rotys: List pf rotations around the y axis to apply to each scatterer
  • rotzs: List pf rotations around the z axis to apply to each scatterer
  • root_path: The folder containing the file, the scatterer to be loaded will be loaded from root_path+path
Returns

A merged mesh from all of the paths provided

def merge_scatterers(*scatterers: vedo.mesh.Mesh, flag: bool = False) -> vedo.mesh.Mesh:
157def merge_scatterers(*scatterers:Mesh, flag:bool=False) ->Mesh:
158    '''
159    Combines any number of scatterers into a single scatterer\n
160    :param scatterers: any number of scatterers to combine
161    :param flag: Value will be passed to `vedo.merge`
162    :return: the combined scatterer
163    '''
164    names = []
165    Fnames = []
166    for scatterer in scatterers:
167        names.append(scatterer_file_name(scatterer))
168        Fnames.append(scatterer.metadata["FILE"][0])
169    
170    if flag:
171        combined = vedo.merge(scatterers, flag=True)
172    else:
173        combined = vedo.merge(scatterers)
174    combined.filename = "".join(names)
175    combined.metadata["FILE"] = "".join(Fnames)
176    return combined

Combines any number of scatterers into a single scatterer

Parameters
  • scatterers: any number of scatterers to combine
  • flag: Value will be passed to vedo.merge
Returns

the combined scatterer

def scale_to_diameter( scatterer: vedo.mesh.Mesh, diameter: float, reset: bool = True, origin: bool = True) -> None:
179def scale_to_diameter(scatterer:Mesh , diameter: float, reset:bool=True, origin:bool=True) -> None:
180    '''
181    Scale a mesh to a given diameter in the x-axis and recomputes normals and areas \n
182    Modifies scatterer in place so does not return anything.\n
183
184    :param scatterer: The scatterer to scale
185    :param diameter: The diameter target
186    '''
187    x1,x2,y1,y2,z1,z2 = scatterer.bounds()
188    diameter_sphere = x2 - x1
189    scatterer.scale(diameter/diameter_sphere,reset=reset, origin=origin)
190    scatterer.compute_cell_size()
191    scatterer.compute_normals()
192    scatterer.filename = scatterer_file_name(scatterer)

Scale a mesh to a given diameter in the x-axis and recomputes normals and areas

Modifies scatterer in place so does not return anything.

Parameters
  • scatterer: The scatterer to scale
  • diameter: The diameter target
def get_diameter(scatterer: vedo.mesh.Mesh):
194def get_diameter(scatterer:Mesh):
195    x1,x2,y1,y2,z1,z2 = scatterer.bounds()
196    diameter_sphere = torch.norm(torch.Tensor([x2,]) - torch.Tensor([x1,]), p=2)
197    return diameter_sphere
def get_plane( scatterer: vedo.mesh.Mesh, origin: tuple[int] = (0, 0, 0), normal: tuple[int] = (1, 0, 0)) -> vedo.mesh.Mesh:
200def get_plane(scatterer: Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> Mesh:
201    '''
202    Get intersection of a scatterer and a plane\n
203    :param scatterer: The scatterer to intersect
204    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
205    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
206    :return: new `Mesh` Containing the intersection of the plane and the scatterer
207    '''
208    intersection = scatterer.clone().intersect_with_plane(origin,normal)
209    intersection.filename = scatterer.filename + "plane" + str(origin)+str(normal)
210    return intersection

Get intersection of a scatterer and a plane

Parameters
  • scatterer: The scatterer to intersect
  • origin: A point on the plane as a tuple (x,y,z). Default (0,0,0)
  • normal: The normal to the plane at point as a tuple (x,y,z). Default (1,0,0)
Returns

new Mesh Containing the intersection of the plane and the scatterer

def get_lines_from_plane( scatterer: vedo.mesh.Mesh, origin: tuple[int] = (0, 0, 0), normal: tuple[int] = (1, 0, 0)) -> list[int]:
212def get_lines_from_plane(scatterer:Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> list[int]:
213    '''
214    Gets the edges on a plane from the intersection between a scatterer and the plane\n
215    :param scatterer: The scatterer to intersect
216    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
217    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
218    :return: a list of edges in the plane 
219    '''
220
221    mask = [0,0,0]
222    for i in range(3):
223        mask[i] =not normal[i]
224    mask = np.array(mask)
225
226    intersection = get_plane(scatterer, origin, normal)
227    verticies = intersection.vertices
228    lines = intersection.lines
229
230    connections = []
231
232    for i in range(len(lines)):
233        connections.append([verticies[lines[i][0]][mask],verticies[lines[i][1]][mask]])
234
235    return connections

Gets the edges on a plane from the intersection between a scatterer and the plane

Parameters
  • scatterer: The scatterer to intersect
  • origin: A point on the plane as a tuple (x,y,z). Default (0,0,0)
  • normal: The normal to the plane at point as a tuple (x,y,z). Default (1,0,0)
Returns

a list of edges in the plane

def plot_plane(connections: list[int]) -> None:
237def plot_plane(connections:list[int]) -> None:
238    '''
239    Plot a set of edges assuming they are co-planar\n
240    :param connections: list of connections to plot
241    '''
242    
243    for con in connections:
244        xs = [con[0][0], con[1][0]]
245        ys = [con[0][1], con[1][1]]
246        plt.plot(xs,ys,color = "blue")
247
248    plt.xlim((-0.06,0.06))
249    plt.ylim((-0.06,0.06))
250    plt.show()

Plot a set of edges assuming they are co-planar

Parameters
  • connections: list of connections to plot
def get_normals_as_points( *scatterers: vedo.mesh.Mesh, permute_to_points: bool = True) -> torch.Tensor:
252def get_normals_as_points(*scatterers:Mesh, permute_to_points:bool=True) -> Tensor:
253    '''
254    Returns the normal vectors to the surface of a scatterer as a `torch` `Tensor` as acoustools points\n
255    :param scatterers: The scatterer to use
256    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
257    :return: normals
258    '''
259    norm_list = []
260    for scatterer in scatterers:
261        scatterer.compute_normals()
262        norm =  torch.tensor(scatterer.cell_normals).to(device)
263
264        if permute_to_points:
265            norm = torch.permute(norm,(1,0))
266        
267        norm_list.append(norm.to(DTYPE))
268    
269    return torch.stack(norm_list)

Returns the normal vectors to the surface of a scatterer as a torch Tensor as acoustools points

Parameters
  • scatterers: The scatterer to use
  • permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
Returns

normals

def get_centre_of_mass_as_points( *scatterers: vedo.mesh.Mesh, permute_to_points: bool = True) -> torch.Tensor:
271def get_centre_of_mass_as_points(*scatterers:Mesh, permute_to_points:bool=True) ->Tensor:
272    '''
273    Returns the centre of mass(es) of a scatterer(s) as a `torch` `Tensor` as acoustools points\n
274    :param scatterers: The scatterer(s) to use
275    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
276    :return: centre of mass(es)
277    '''
278    centres_list = []
279    for scatterer in scatterers:
280        centre_of_mass =  torch.tensor(scatterer.center_of_mass()).to(DTYPE).to(device)
281
282        if permute_to_points:
283            centre_of_mass = torch.unsqueeze(centre_of_mass,1)
284        
285        centres_list.append(centre_of_mass.to(DTYPE))
286    
287    return torch.real(torch.stack(centres_list))

Returns the centre of mass(es) of a scatterer(s) as a torch Tensor as acoustools points

Parameters
  • scatterers: The scatterer(s) to use
  • permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
Returns

centre of mass(es)

def get_centres_as_points( *scatterers: vedo.mesh.Mesh, permute_to_points: bool = True, add_normals: bool = False, normal_scale: float = 0.001) -> torch.Tensor:
290def get_centres_as_points(*scatterers:Mesh, permute_to_points:bool=True, add_normals:bool=False, normal_scale:float=0.001) ->Tensor:
291    '''
292    Returns the centre of scatterer faces as a `torch` `Tensor` as acoustools points\n
293    :param scatterers: The scatterer to use
294    :param permute_to_points: If `True` will permute the order of coordinates to agree with what acoustools expects.
295    :return: centres
296    '''
297    centre_list = []
298    for scatterer in scatterers:
299        centres =  torch.tensor(scatterer.cell_centers().points).to(DTYPE).to(device)
300
301        if permute_to_points:
302            centres = torch.permute(centres,(1,0)).unsqueeze_(0)
303        
304        if add_normals:
305            norms= get_normals_as_points(scatterer)
306            centres += norms.real * normal_scale
307        
308        centre_list.append(centres)
309    centres = torch.cat(centre_list,dim=0)
310    return centres

Returns the centre of scatterer faces as a torch Tensor as acoustools points

Parameters
  • scatterers: The scatterer to use
  • permute_to_points: If True will permute the order of coordinates to agree with what acoustools expects.
Returns

centres

def get_verticies_as_points(*scatterers: vedo.mesh.Mesh):
312def get_verticies_as_points(*scatterers:Mesh):
313    '''
314    Gets the verticies of a mesh as a Tensor of AcousTools (B,3,N) points \n
315    :param Mesh: Mesh to use
316    :returns verticies: verticies as points
317    '''
318   
319    vert_list = []
320    for scatterer in scatterers:
321        vert =  torch.tensor(scatterer.vertices).to(DTYPE).to(device)
322        vert_list.append(vert)
323
324    verts = torch.cat(vert_list,dim=0).unsqueeze(0).permute(0,2,1)
325    return verts

Gets the verticies of a mesh as a Tensor of AcousTools (B,3,N) points

Parameters
  • Mesh: Mesh to use :returns verticies: verticies as points
def get_cell_verticies(*scatterers: vedo.mesh.Mesh):
327def get_cell_verticies(*scatterers:Mesh):
328    '''
329    Gets a tensor of (B,3,M,3) - batch x (xyz) x Faces x (vertex) \n
330    :param Mesh: Mesh to use
331    :returns verticies: verticies
332    '''
333    verts = get_verticies_as_points(*scatterers)
334    vert_list = []
335    for scatterer in scatterers:
336        cells = torch.tensor(scatterer.cells)
337        N = cells.shape[0]
338        cell_indexes = cells.flatten()
339        cell_verts = torch.index_select(verts, 2, cell_indexes)
340        cell_verts=cell_verts.reshape(1,3,N,3)
341
342
343        vert_list.append(cell_verts)
344    verts = torch.cat(vert_list,dim=0)
345    return verts

Gets a tensor of (B,3,M,3) - batch x (xyz) x Faces x (vertex)

Parameters
  • Mesh: Mesh to use :returns verticies: verticies
def get_areas(*scatterers: vedo.mesh.Mesh) -> torch.Tensor:
380def get_areas(*scatterers: Mesh) -> Tensor:
381    '''
382    Returns the areas of faces of any number of scatterers\n
383    :param scatterers: The scatterers to use.
384    :return: areas
385    '''
386    area_list = []
387    for scatterer in scatterers:
388        scatterer.compute_cell_size()
389        area_list.append(torch.Tensor(scatterer.celldata["Area"]).to(device))
390    
391    return torch.stack(area_list)

Returns the areas of faces of any number of scatterers

Parameters
  • scatterers: The scatterers to use.
Returns

areas

def get_weight( scatterer: vedo.mesh.Mesh, density: float = 29.36, g: float = 9.81) -> float:
393def get_weight(scatterer:Mesh, density:float=Constants.p_p, g:float=9.81) -> float:
394    '''
395    Get the weight of a scatterer\\
396    :param scatterer: The scatterer to use\\
397    :param density: The density to use. Default density for EPS\\
398    :param g: value for g to use. Default 9.81\\
399    :return: weight
400    '''
401    mass = scatterer.volume() * density
402    return g * mass

Get the weight of a scatterer\

Parameters
  • scatterer: The scatterer to use\
  • density: The density to use. Default density for EPS\
  • g: value for g to use. Default 9.81\
Returns

weight

def translate( scatterer: vedo.mesh.Mesh, dx: float = 0, dy: float = 0, dz: float = 0) -> None:
404def translate(scatterer:Mesh, dx:float=0,dy:float=0,dz:float=0) -> None:
405    '''
406    Translates a scatterer by (dx,dy,dz) \n
407    Modifies inplace so does not return a value \n
408    :param scatterer: The scatterer to use
409    :param dx: Translation in the x direction
410    :param dy: Translation in the y direction
411    :param dz: Translation in the z direction
412    '''
413    scatterer.shift(np.array([dx,dy,dz]))
414    scatterer.filename = scatterer_file_name(scatterer)

Translates a scatterer by (dx,dy,dz)

Modifies inplace so does not return a value

Parameters
  • scatterer: The scatterer to use
  • dx: Translation in the x direction
  • dy: Translation in the y direction
  • dz: Translation in the z direction
def rotate( scatterer: vedo.mesh.Mesh, axis: tuple[int], rot: float, centre: tuple[int] = (0, 0, 0), rotate_around_COM: bool = False):
416def rotate(scatterer:Mesh, axis:tuple[int], rot:float, centre:tuple[int]=(0, 0, 0), rotate_around_COM:bool=False):
417    '''
418    Rotates a scatterer in axis by rot\n
419    Modifies inplace so does not return a value\n
420    :param scatterer: The scatterer to use
421    :param axis: The axis to rotate in
422    :param rot: Angle to rotate in degrees
423    :param centre: point to rotate around
424    :param rotate_around_COM: If True will set `centre` to `scatterer`s centre of mass
425    '''
426    if rotate_around_COM:
427        centre = vedo.vector(get_centre_of_mass_as_points(scatterer).cpu().detach().squeeze())
428
429    if axis[0]:
430        scatterer.metadata["rotX"] = scatterer.metadata["rotX"] + rot
431    if axis[1]:
432        scatterer.metadata["rotY"] = scatterer.metadata["rotY"] + rot
433    if axis[2]:
434        scatterer.metadata["rotZ"] = scatterer.metadata["rotZ"] + rot
435    scatterer.rotate(rot, axis,point=centre)
436    scatterer.filename = scatterer_file_name(scatterer)

Rotates a scatterer in axis by rot

Modifies inplace so does not return a value

Parameters
  • scatterer: The scatterer to use
  • axis: The axis to rotate in
  • rot: Angle to rotate in degrees
  • centre: point to rotate around
  • rotate_around_COM: If True will set centre to scatterers centre of mass
def downsample( scatterer: vedo.mesh.Mesh, factor: int = 2, n: int | None = None, method: str = 'quadric', boundaries: bool = False, compute_areas: bool = True, compute_normals: bool = True) -> vedo.mesh.Mesh:
439def downsample(scatterer:Mesh, factor:int=2, n:int|None=None, method:str='quadric', boundaries:bool=False, compute_areas:bool=True, compute_normals:bool=True) -> Mesh:
440    '''
441    Downsamples a mesh to have `factor` less elements\n
442    :param scatterer: The scatterer to use
443    :param factor: The factor to downsample by
444    :param n: The desired number of final points, passed to `Vedo.Mesh.decimate`
445    :param method:, `boundaries` - passed to `vedo.decimate`
446    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default `True`
447    :param compute_normals: if true will call `scatterer.compute_normals()`. Default `True`
448    :return: downsampled mesh
449    '''
450    scatterer_small =  scatterer.decimate(1/factor, n, method, boundaries)
451    
452    scatterer_small.metadata["rotX"] = scatterer.metadata["rotX"]
453    scatterer_small.metadata["rotY"] = scatterer.metadata["rotY"]
454    scatterer_small.metadata["rotZ"] = scatterer.metadata["rotZ"]
455
456    if compute_areas: scatterer_small.compute_cell_size()
457    if compute_normals: 
458        scatterer_small.compute_normals()
459
460    scatterer_small.filename = scatterer_file_name(scatterer_small)  + "-scale-" + str(factor)
461
462
463    return scatterer_small

Downsamples a mesh to have factor less elements

Parameters
  • scatterer: The scatterer to use
  • factor: The factor to downsample by
  • n: The desired number of final points, passed to Vedo.Mesh.decimate
  • method: , boundaries - passed to vedo.decimate
  • compute_areas: if true will call scatterer.compute_cell_size(). Default True
  • compute_normals: if true will call scatterer.compute_normals(). Default True
Returns

downsampled mesh

def centre_scatterer(scatterer: vedo.mesh.Mesh) -> list[int]:
466def centre_scatterer(scatterer:Mesh) -> list[int]:
467    '''
468    Translate scatterer so the centre of mass is at (0,0,0)\n
469    Modifies Mesh in place \n
470    :param scatterer: Scatterer to centre
471    :return: Returns the amount needed to move in each direction
472    '''
473    com = get_centre_of_mass_as_points(scatterer).cpu()
474    correction = [-1*com[:,0].item(), -1*com[:,1].item(), -1*com[:,2].item()]
475    translate(scatterer, dx = correction[0], dy = correction[1], dz=  correction[2])
476
477    return correction

Translate scatterer so the centre of mass is at (0,0,0)

Modifies Mesh in place

Parameters
  • scatterer: Scatterer to centre
Returns

Returns the amount needed to move in each direction

def get_edge_data( scatterer: vedo.mesh.Mesh, wavelength: float = 0.008575, print_output: bool = True, break_down_average: bool = False) -> None | tuple[float]:
480def get_edge_data(scatterer:Mesh, wavelength:float=Constants.wavelength, print_output:bool=True, break_down_average:bool=False) -> None|tuple[float]:
481    '''
482    Get the maximum, minimum and average size of edges in a mesh. Optionally prints or returns the result.\n
483    :param scatterer: Mesh of interest
484    :param wavelength: Wavenelgth size for printing results as multiple of some wavelength
485    :param print_output: If True, prints results else returns values
486    :break_down_average: If True will also return (distance_sum, N)
487    :return: None if `print_outputs` is `True` else returns `(max_distance, min_distance, average_distance)` and optionally  (distance_sum, N)
488
489    '''
490    points = scatterer.vertices
491
492    distance_sum = 0
493    N = 0
494
495    max_distance = 0
496    min_distance = 100000000
497
498
499    for (start,end) in scatterer.edges:
500        start_point = points[start]
501        end_point = points[end]
502        sqvec = torch.Tensor((start_point-end_point)**2)
503        # print(sqvec, torch.sum(sqvec)**0.5)
504        distance = torch.sum(sqvec)**0.5
505        distance_sum += distance
506        N += 1
507        if distance < min_distance:
508            min_distance = distance
509        if distance > max_distance:
510            max_distance = distance
511
512    average_distance = distance_sum/N
513
514    if print_output:
515        print('Max Distance', max_distance.item(),'=' ,max_distance.item()/wavelength, 'lambda')
516        print('Min Distance', min_distance.item(),'=', min_distance.item()/wavelength, 'lambda')
517        print('Ave Distance', average_distance.item(),'=', average_distance.item()/wavelength, 'lambda')
518    else:
519        if break_down_average:
520            return (max_distance, min_distance, average_distance), (distance_sum, N)
521        else:
522            return (max_distance, min_distance, average_distance)

Get the maximum, minimum and average size of edges in a mesh. Optionally prints or returns the result.

Parameters
  • scatterer: Mesh of interest
  • wavelength: Wavenelgth size for printing results as multiple of some wavelength
  • print_output: If True, prints results else returns values :break_down_average: If True will also return (distance_sum, N)
Returns

None if print_outputs is True else returns (max_distance, min_distance, average_distance) and optionally (distance_sum, N)

def cut_mesh_to_walls( scatterer: vedo.mesh.Mesh, layer_z: float, layer_normal: tuple[float] = (0, 0, -1.0), wall_thickness=0.001) -> vedo.mesh.Mesh:
525def cut_mesh_to_walls(scatterer:Mesh, layer_z:float, layer_normal:tuple[float] = (0,0,-1.0), wall_thickness = 0.001) -> Mesh:
526    '''
527    Cuts a mesh with a given plane and then converts the result to have walls of a certain thickness \n
528    :param scatterer: Mesh to use
529    :param layer_z: coordinate of layer
530    :param layer_normal: Normal to layer (if not +- (0,0,1) then layer_z will not refer to a z coordinate)
531    :param wall_thickness: Thickness of the walls to returns
532    :return: Cut mesh with walls
533    '''
534
535    xmin,xmax, ymin,ymax, zmin,zmax = scatterer.bounds()
536    dx = xmax-xmin
537    dy = ymax-ymin
538
539    scale_x = (dx-2*wall_thickness) / dx
540    scale_y = (dy-2*wall_thickness) / dy
541
542    outler_layer = scatterer.cut_with_plane((0,0,layer_z),layer_normal)
543    inner_layer = outler_layer.clone()
544    inner_layer.scale((scale_x,scale_y,1), origin=False)
545
546    com_outer = get_centre_of_mass_as_points(outler_layer)
547    com_inner = get_centre_of_mass_as_points(inner_layer)
548
549    d_com = (com_outer - com_inner).squeeze()
550
551    translate(inner_layer, *d_com)
552
553    walls = vedo.merge(outler_layer, inner_layer)
554
555
556    boundaries_outer = outler_layer.boundaries()
557    boundaries_inner = inner_layer.boundaries()
558
559    strips = boundaries_outer.join_with_strips(boundaries_inner).triangulate()
560    
561
562    walls = vedo.merge(walls,strips)
563    
564    calculate_features(walls)
565    scatterer_file_name(walls)
566
567    return walls.clean()

Cuts a mesh with a given plane and then converts the result to have walls of a certain thickness

Parameters
  • scatterer: Mesh to use
  • layer_z: coordinate of layer
  • layer_normal: Normal to layer (if not +- (0,0,1) then layer_z will not refer to a z coordinate)
  • wall_thickness: Thickness of the walls to returns
Returns

Cut mesh with walls

def cut_closed_scatterer(scatterer: vedo.mesh.Mesh, layer_z: float, normals=[(0, 0, 1)]):
569def cut_closed_scatterer(scatterer:Mesh,layer_z:float, normals=[(0,0,1)]):
570    '''
571    Cuts a scatterer across a z-plane\\
572    :param scatterer: Mesh
573    :param layer_z: height to cute
574    :param normals: Which way is up 
575    '''
576    origins=[(0,0,layer_z)]
577    closed_scatterer = scatterer.cut_closed_surface(origins=origins, normals=normals)
578    return closed_scatterer

Cuts a scatterer across a z-plane\

Parameters
  • scatterer: Mesh
  • layer_z: height to cute
  • normals: Which way is up
def get_volume(scatterer: vedo.mesh.Mesh):
580def get_volume(scatterer:Mesh):
581    '''
582    Returns the volume of a mesh
583    '''
584    return scatterer.volume()

Returns the volume of a mesh

def insert_parasite( scatterer: vedo.mesh.Mesh, parasite_path: str = '/Sphere-lam1.stl', root_path: str = '../BEMMedia', parasite_size: float = 0.00214375, parasite_offset: torch.Tensor = None) -> vedo.mesh.Mesh:
586def insert_parasite(scatterer:Mesh, parasite_path:str = '/Sphere-lam1.stl', root_path:str="../BEMMedia", parasite_size:float=Constants.wavelength/4, parasite_offset:Tensor=None) -> Mesh:
587    '''
588    Inserts a parasitic body into an existing scatterer. Used to supress the resonance from BEM \n
589    See https://doi.org/10.1109/8.310000 \n
590    :param scatterer: The scatterer to insert parasite into
591    :param parasite_path: The path to the mesh to load and use as parasite
592    :param root_path: The folder to load the file from
593    :param parasite_size: The diameter to scale the parasite to
594    :param parasite_offset: Tensor of offsets for the parasite from the (0,0,0) point
595    :returns: Scatterer with parasite inserted
596    '''
597    parasite = load_scatterer(parasite_path, root_path=root_path)
598    centre_scatterer(parasite)
599    if parasite_offset is None:
600        parasite_offset = get_centre_of_mass_as_points(scatterer)
601
602    dx = parasite_offset[:,0].item()
603    dy = parasite_offset[:,1].item()
604    dz = parasite_offset[:,2].item()
605
606    translate(parasite, dx=dx, dy=dy, dz=dz)
607
608    scale_to_diameter(parasite, parasite_size)
609
610    infected_scatterer = merge_scatterers(scatterer, parasite)
611
612    return infected_scatterer

Inserts a parasitic body into an existing scatterer. Used to supress the resonance from BEM

See https://doi.org/10.1109/8.310000

Parameters
  • scatterer: The scatterer to insert parasite into
  • parasite_path: The path to the mesh to load and use as parasite
  • root_path: The folder to load the file from
  • parasite_size: The diameter to scale the parasite to
  • parasite_offset: Tensor of offsets for the parasite from the (0,0,0) point :returns: Scatterer with parasite inserted
def get_CHIEF_points( scatterer: vedo.mesh.Mesh, P=30, method: Literal['random', 'uniform', 'volume-random'] = 'random', start: Literal['surface', 'centre'] = 'surface', scale=0.001, scale_mode: Literal['abs', 'diameter-scale'] = 'abs') -> vedo.mesh.Mesh:
614def get_CHIEF_points(scatterer:Mesh, P=30, method:Literal['random', 'uniform', 'volume-random']='random', start:Literal['surface', 'centre']='surface', scale=0.001, scale_mode:Literal['abs','diameter-scale']='abs') -> Mesh:
615    '''
616    Generates internal points that can be used for the CHIEF BEM formulation (or any other reason)\n
617    :param scatterer: The scatterer to insert points into
618    :param P: Number of points. if P=-1 then P= number of mesh elements
619    :param method: The method used to generate points \n
620        - random: will move scale metres along each of P randomly selected normals \n
621        - uniform:  will move scale metres along each of P uniformly spaced normals (based on order coming from `Mesh.get_normals_as_points`) \n
622        - volume-random: will use `vedo.Mesh..generate_random_points` to generate P internal points
623    :param start: The point to use as the basis for generating points \n
624         - surface: Will step along normals from surface (will step in the -ve normal direction)
625         - centre: Will step along normal from centre of mass (will step in +ve normal direction)
626    :param scale: The distance in m to step 
627    :returns internal points:
628    '''
629
630    centre_norms = get_normals_as_points(scatterer, permute_to_points=False)
631
632    if scale_mode.lower() == 'diameter-scale':
633        d = get_diameter(scatterer)
634        scale = scale * d
635    
636
637    if start.lower() == 'centre':
638        centres = get_centre_of_mass_as_points(scatterer, permute_to_points=False).unsqueeze(1)
639        internal_points = centres + centre_norms * scale      
640
641    else:
642        centres = torch.tensor(scatterer.cell_centers().points, dtype=DTYPE, device=device)
643        internal_points = centres - centre_norms * scale
644
645    M = centre_norms.shape[1]
646    
647    if P == -1: P = M
648
649   
650
651
652    
653    if method.lower() == 'random':
654        indices = torch.randperm(M)[:P]
655        internal_points = internal_points[:, indices,:]
656
657    elif method.lower()== 'uniform':
658        idx = [i for i in range(M) if i%(int(M/P)) == 0]
659        internal_points = internal_points[:, idx,:]
660    elif method.lower() == 'volume-random':
661        internal_points = torch.Tensor(scatterer.generate_random_points(P).points).unsqueeze(0)
662
663    internal_points = internal_points.permute(0,2,1)
664
665
666    return internal_points

Generates internal points that can be used for the CHIEF BEM formulation (or any other reason)

Parameters
  • scatterer: The scatterer to insert points into
  • P: Number of points. if P=-1 then P= number of mesh elements
  • method: The method used to generate points

    • random: will move scale metres along each of P randomly selected normals

    • uniform: will move scale metres along each of P uniformly spaced normals (based on order coming from Mesh.get_normals_as_points)

    • volume-random: will use vedo.Mesh..generate_random_points to generate P internal points

  • start: The point to use as the basis for generating points

    • surface: Will step along normals from surface (will step in the -ve normal direction)
    • centre: Will step along normal from centre of mass (will step in +ve normal direction)
  • scale: The distance in m to step :returns internal points: