src.acoustools.Mesh

  1from acoustools.Utilities import device, DTYPE
  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
 10
 11
 12def board_name(board:Tensor) -> str:
 13    '''
 14    Returns the name for a board, TOP and/or BOTTOM, used in cache system
 15    :param board: The board to use
 16    :return: name of board as `<'TOP'><'BOTTOM'><M>` for `M` transducers in the boards 
 17    '''
 18    M = board.shape[0]
 19
 20    top = "TOP" if 1 in torch.sign(board[:,2]) else ""
 21    bottom = "BOTTOM" if -1 in torch.sign(board[:,2]) else ""
 22    return top+bottom+str(M)
 23
 24def scatterer_file_name(scatterer:Mesh) ->str:
 25    '''
 26    Get a unique name to describe a scatterer position, calls `str(scatterer.coordinates)`
 27    ONLY USE TO SET FILENAME, USE `scatterer.filename` TO GET
 28    :param scatterer: The Mesh to use
 29    :return: Scatterer name
 30    
 31    '''
 32
 33    f_name = str(list(scatterer.coordinates))
 34    return f_name
 35
 36def load_scatterer(path:str, compute_areas:bool = True, compute_normals:bool=True, dx:float=0,
 37                   dy:float=0,dz:float=0, rotx:float=0, roty:float=0, rotz:float=0, root_path:str="", force:bool=False) -> Mesh:
 38    '''
 39    Loads a scatterer as a `vedo` `Mesh` and applies translations as needed
 40    :param path: The name of the scatterer to load
 41    :param compute_areas: if `True` will call `scatterer.compute_cell_size()`. Default `True`
 42    :param compute_normals: if `True` will call `scatterer.compute_normals()`. Default `True`
 43    :param dx: Translation in the x direction to apply
 44    :param dy: Translation in the y direction to apply
 45    :param dz: Translation in the z direction to apply
 46    :param rotx: Rotation around the x axis to apply
 47    :param roty: Rotation around the y axis to apply
 48    :param rotz: Rotation around the z axis to apply
 49    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
 50    :return: The `vedo` `Mesh` of the scatterer
 51    '''
 52    scatterer = vedo.load(root_path+path, force=force)
 53    
 54    if scatterer is not None:
 55        if compute_areas: scatterer.compute_cell_size()
 56        if compute_normals: 
 57            scatterer.compute_normals()
 58
 59        scatterer.metadata["rotX"] = 0
 60        scatterer.metadata["rotY"] = 0
 61        scatterer.metadata["rotZ"] = 0
 62
 63        # scatterer.filename = scatterer.filename.split("/")[-1]
 64        scatterer.filename = scatterer_file_name(scatterer)
 65
 66        scatterer.metadata["FILE"] = scatterer.filename.split(".")[0]
 67
 68
 69        rotate(scatterer,(1,0,0),rotx)
 70        rotate(scatterer,(0,1,0),roty)
 71        rotate(scatterer,(0,0,1),rotz)
 72
 73        translate(scatterer,dx,dy,dz)
 74
 75
 76    return scatterer
 77
 78def calculate_features(scatterer:Mesh, compute_areas:bool = True, compute_normals:bool=True):
 79    '''
 80    @private
 81    '''
 82    if compute_areas: scatterer.compute_cell_size()
 83    if compute_normals: scatterer.compute_normals()
 84
 85    scatterer.filename = scatterer_file_name(scatterer)
 86    scatterer.metadata["FILE"] = scatterer.filename.split(".")[0]
 87
 88
 89def load_multiple_scatterers(paths:list[str],  compute_areas:bool = True, compute_normals:bool=True, 
 90                             dxs:list[int]=[],dys:list[int]=[],dzs:list[int]=[], rotxs:list[int]=[], rotys:list[int]=[], rotzs:list[int]=[], root_path:str="") -> Mesh:
 91    '''
 92    Loads multiple scatterers and combines them into a single scatterer object
 93    :param path: The name of the scatterers to load
 94    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default True
 95    :param compute_normals: if true will call `scatterer.compute_normals()`. Default True
 96    :param dxs: List of translations in the x direction to apply to each scatterer
 97    :param dys: List of translations in the y direction to apply to each scatterer
 98    :param dzs: List of translations in the z direction to apply to each scatterer
 99    :param rotxs: List pf rotations around the x axis to apply to each scatterer
100    :param rotys: List pf rotations around the y axis to apply to each scatterer
101    :param rotzs: List pf rotations around the z axis to apply to each scatterer
102    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
103    :return: A merged mesh from all of the paths provided
104    '''
105    dxs += [0] * (len(paths) - len(dxs))
106    dys += [0] * (len(paths) - len(dys))
107    dzs += [0] * (len(paths) - len(dzs))
108
109    rotxs += [0] * (len(paths) - len(rotxs))
110    rotys += [0] * (len(paths) - len(rotys))
111    rotzs += [0] * (len(paths) - len(rotzs))
112
113    scatterers = []
114    for i,path in enumerate(paths):
115        scatterer = load_scatterer(path, compute_areas, compute_normals, dxs[i],dys[i],dzs[i],rotxs[i],rotys[i],rotzs[i],root_path)
116        scatterers.append(scatterer)
117    combined = merge_scatterers(*scatterers)
118    return combined
119
120def merge_scatterers(*scatterers:Mesh, flag:bool=False) ->Mesh:
121    '''
122    Combines any number of scatterers into a single scatterer\n
123    :param scatterers: any number of scatterers to combine
124    :param flag: Value will be passed to `vedo.merge`
125    :return: the combined scatterer
126    '''
127    names = []
128    Fnames = []
129    for scatterer in scatterers:
130        names.append(scatterer_file_name(scatterer))
131        Fnames.append(scatterer.metadata["FILE"][0])
132    
133    if flag:
134        combined = vedo.merge(scatterers, flag=True)
135    else:
136        combined = vedo.merge(scatterers)
137    combined.filename = "".join(names)
138    combined.metadata["FILE"] = "".join(Fnames)
139    return combined
140
141
142def scale_to_diameter(scatterer:Mesh , diameter: float, reset:bool=True, origin:bool=True) -> None:
143    '''
144    Scale a mesh to a given diameter in the x-axis and recomputes normals and areas \n
145    Modifies scatterer in place so does not return anything.\n
146
147    :param scatterer: The scatterer to scale
148    :param diameter: The diameter target
149    '''
150    x1,x2,y1,y2,z1,z2 = scatterer.bounds()
151    diameter_sphere = x2 - x1
152    scatterer.scale(diameter/diameter_sphere,reset=reset, origin=origin)
153    scatterer.compute_cell_size()
154    scatterer.compute_normals()
155    scatterer.filename = scatterer_file_name(scatterer)
156    
157
158def get_plane(scatterer: Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> Mesh:
159    '''
160    Get intersection of a scatterer and a plane\n
161    :param scatterer: The scatterer to intersect
162    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
163    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
164    :return: new `Mesh` Containing the intersection of the plane and the scatterer
165    '''
166    intersection = scatterer.clone().intersect_with_plane(origin,normal)
167    intersection.filename = scatterer.filename + "plane" + str(origin)+str(normal)
168    return intersection
169
170def get_lines_from_plane(scatterer:Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> list[int]:
171    '''
172    Gets the edges on a plane from the intersection between a scatterer and the plane\n
173    :param scatterer: The scatterer to intersect
174    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
175    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
176    :return: a list of edges in the plane 
177    '''
178
179    mask = [0,0,0]
180    for i in range(3):
181        mask[i] =not normal[i]
182    mask = np.array(mask)
183
184    intersection = get_plane(scatterer, origin, normal)
185    verticies = intersection.vertices
186    lines = intersection.lines
187
188    connections = []
189
190    for i in range(len(lines)):
191        connections.append([verticies[lines[i][0]][mask],verticies[lines[i][1]][mask]])
192
193    return connections
194
195def plot_plane(connections:list[int]) -> None:
196    '''
197    Plot a set of edges assuming they are co-planar\n
198    :param connections: list of connections to plot
199    '''
200    
201    for con in connections:
202        xs = [con[0][0], con[1][0]]
203        ys = [con[0][1], con[1][1]]
204        plt.plot(xs,ys,color = "blue")
205
206    plt.xlim((-0.06,0.06))
207    plt.ylim((-0.06,0.06))
208    plt.show()
209
210def get_normals_as_points(*scatterers:Mesh, permute_to_points:bool=True) -> Tensor:
211    '''
212    Returns the normal vectors to the surface of a scatterer as a `torch` `Tensor` as acoustools points\n
213    :param scatterers: The scatterer to use
214    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
215    :return: normals
216    '''
217    norm_list = []
218    for scatterer in scatterers:
219        scatterer.compute_normals()
220        norm =  torch.tensor(scatterer.cell_normals).to(device)
221
222        if permute_to_points:
223            norm = torch.permute(norm,(1,0))
224        
225        norm_list.append(norm.to(DTYPE))
226    
227    return torch.stack(norm_list)
228
229def get_centre_of_mass_as_points(*scatterers:Mesh, permute_to_points:bool=True) ->Tensor:
230    '''
231    Returns the centre of mass(es) of a scatterer(s) as a `torch` `Tensor` as acoustools points\n
232    :param scatterers: The scatterer(s) to use
233    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
234    :return: centre of mass(es)
235    '''
236    centres_list = []
237    for scatterer in scatterers:
238        centre_of_mass =  torch.tensor(scatterer.center_of_mass()).to(device)
239
240        if permute_to_points:
241            centre_of_mass = torch.unsqueeze(centre_of_mass,1)
242        
243        centres_list.append(centre_of_mass.to(DTYPE))
244    
245    return torch.real(torch.stack(centres_list))
246
247
248def get_centres_as_points(*scatterers:Mesh, permute_to_points:bool=True, add_normals:bool=False, normal_scale:float=0.001) ->Tensor:
249    '''
250    Returns the centre of scatterer faces as a `torch` `Tensor` as acoustools points\n
251    :param scatterers: The scatterer to use
252    :param permute_to_points: If `True` will permute the order of coordinates to agree with what acoustools expects.
253    :return: centres
254    '''
255    centre_list = []
256    for scatterer in scatterers:
257        centres =  torch.tensor(scatterer.cell_centers().points).to(device)
258
259        if permute_to_points:
260            centres = torch.permute(centres,(1,0)).unsqueeze_(0)
261        
262        if add_normals:
263            norms= get_normals_as_points(scatterer)
264            centres += norms.real * normal_scale
265        
266        centre_list.append(centres.to(DTYPE))
267    centres = torch.cat(centre_list,dim=0)
268    return centres
269
270def get_areas(*scatterers: Mesh) -> Tensor:
271    '''
272    Returns the areas of faces of any number of scatterers\n
273    :param scatterers: The scatterers to use.
274    :return: areas
275    '''
276    area_list = []
277    for scatterer in scatterers:
278        scatterer.compute_cell_size()
279        area_list.append(torch.Tensor(scatterer.celldata["Area"]).to(device))
280    
281    return torch.stack(area_list)
282
283def get_weight(scatterer:Mesh, density:float=Constants.p_p, g:float=9.81) -> float:
284    '''
285    Get the weight of a scatterer\\
286    :param scatterer: The scatterer to use\\
287    :param density: The density to use. Default density for EPS\\
288    :param g: value for g to use. Default 9.81\\
289    :return: weight
290    '''
291    mass = scatterer.volume() * density
292    return g * mass
293
294def translate(scatterer:Mesh, dx:float=0,dy:float=0,dz:float=0) -> None:
295    '''
296    Translates a scatterer by (dx,dy,dz) \n
297    Modifies inplace so does not return a value \n
298    :param scatterer: The scatterer to use
299    :param dx: Translation in the x direction
300    :param dy: Translation in the y direction
301    :param dz: Translation in the z direction
302    '''
303    scatterer.shift(np.array([dx,dy,dz]))
304    scatterer.filename = scatterer_file_name(scatterer)
305
306def rotate(scatterer:Mesh, axis:tuple[int], rot:float, centre:tuple[int]=(0, 0, 0), rotate_around_COM:bool=False):
307    '''
308    Rotates a scatterer in axis by rot\n
309    Modifies inplace so does not return a value\n
310    :param scatterer: The scatterer to use
311    :param axis: The axis to rotate in
312    :param rot: Angle to rotate in degrees
313    :param centre: point to rotate around
314    :param rotate_around_COM: If True will set `centre` to `scatterer`s centre of mass
315    '''
316    if rotate_around_COM:
317        centre = vedo.vector(get_centre_of_mass_as_points(scatterer).cpu().detach().squeeze())
318
319    if axis[0]:
320        scatterer.metadata["rotX"] = scatterer.metadata["rotX"] + rot
321    if axis[1]:
322        scatterer.metadata["rotY"] = scatterer.metadata["rotY"] + rot
323    if axis[2]:
324        scatterer.metadata["rotZ"] = scatterer.metadata["rotZ"] + rot
325    scatterer.rotate(rot, axis,point=centre)
326    scatterer.filename = scatterer_file_name(scatterer)
327
328 
329def 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:
330    '''
331    Downsamples a mesh to have `factor` less elements\n
332    :param scatterer: The scatterer to use
333    :param factor: The factor to downsample by
334    :param n: The desired number of final points, passed to `Vedo.Mesh.decimate`
335    :param method:, `boundaries` - passed to `vedo.decimate`
336    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default `True`
337    :param compute_normals: if true will call `scatterer.compute_normals()`. Default `True`
338    :return: downsampled mesh
339    '''
340    scatterer_small =  scatterer.decimate(1/factor, n, method, boundaries)
341    
342    scatterer_small.metadata["rotX"] = scatterer.metadata["rotX"]
343    scatterer_small.metadata["rotY"] = scatterer.metadata["rotY"]
344    scatterer_small.metadata["rotZ"] = scatterer.metadata["rotZ"]
345
346    if compute_areas: scatterer_small.compute_cell_size()
347    if compute_normals: 
348        scatterer_small.compute_normals()
349
350    scatterer_small.filename = scatterer_file_name(scatterer_small)  + "-scale-" + str(factor)
351
352
353    return scatterer_small
354
355
356def centre_scatterer(scatterer:Mesh) -> list[int]:
357    '''
358    Translate scatterer so the centre of mass is at (0,0,0)\n
359    Modifies Mesh in place \n
360    :param scatterer: Scatterer to centre
361    :return: Returns the amount needed to move in each direction
362    '''
363    com = get_centre_of_mass_as_points(scatterer).cpu()
364    correction = [-1*com[:,0].item(), -1*com[:,1].item(), -1*com[:,2].item()]
365    translate(scatterer, dx = correction[0], dy = correction[1], dz=  correction[2])
366
367    return correction
368
369
370def get_edge_data(scatterer:Mesh, wavelength:float=Constants.wavelength, print_output:bool=True, break_down_average:bool=False) -> None|tuple[float]:
371    '''
372    Get the maximum, minimum and average size of edges in a mesh. Optionally prints or returns the result.\n
373    :param scatterer: Mesh of interest
374    :param wavelength: Wavenelgth size for printing results as multiple of some wavelength
375    :param print_output: If True, prints results else returns values
376    :break_down_average: If True will also return (distance_sum, N)
377    :return: None if `print_outputs` is `True` else returns `(max_distance, min_distance, average_distance)` and optionally  (distance_sum, N)
378
379    '''
380    points = scatterer.vertices
381
382    distance_sum = 0
383    N = 0
384
385    max_distance = 0
386    min_distance = 100000000
387
388
389    for (start,end) in scatterer.edges:
390        start_point = points[start]
391        end_point = points[end]
392        sqvec = torch.Tensor((start_point-end_point)**2)
393        # print(sqvec, torch.sum(sqvec)**0.5)
394        distance = torch.sum(sqvec)**0.5
395        distance_sum += distance
396        N += 1
397        if distance < min_distance:
398            min_distance = distance
399        if distance > max_distance:
400            max_distance = distance
401
402    average_distance = distance_sum/N
403
404    if print_output:
405        print('Max Distance', max_distance.item(),'=' ,max_distance.item()/wavelength, 'lambda')
406        print('Min Distance', min_distance.item(),'=', min_distance.item()/wavelength, 'lambda')
407        print('Ave Distance', average_distance.item(),'=', average_distance.item()/wavelength, 'lambda')
408    else:
409        if break_down_average:
410            return (max_distance, min_distance, average_distance), (distance_sum, N)
411        else:
412            return (max_distance, min_distance, average_distance)
413
414
415def cut_mesh_to_walls(scatterer:Mesh, layer_z:float, layer_normal:tuple[float] = (0,0,-1.0), wall_thickness = 0.001) -> Mesh:
416    '''
417    Cuts a mesh with a given plane and then converts the result to have walls of a certain thickness \n
418    :param scatterer: Mesh to use
419    :param layer_z: coordinate of layer
420    :param layer_normal: Normal to layer (if not +- (0,0,1) then layer_z will not refer to a z coordinate)
421    :param wall_thickness: Thickness of the walls to returns
422    :return: Cut mesh with walls
423    '''
424
425    xmin,xmax, ymin,ymax, zmin,zmax = scatterer.bounds()
426    dx = xmax-xmin
427    dy = ymax-ymin
428
429    scale_x = (dx-2*wall_thickness) / dx
430    scale_y = (dy-2*wall_thickness) / dy
431
432    outler_layer = scatterer.cut_with_plane((0,0,layer_z),layer_normal)
433    inner_layer = outler_layer.clone()
434    inner_layer.scale((scale_x,scale_y,1), origin=False)
435
436    com_outer = get_centre_of_mass_as_points(outler_layer)
437    com_inner = get_centre_of_mass_as_points(inner_layer)
438
439    d_com = (com_outer - com_inner).squeeze()
440
441    translate(inner_layer, *d_com)
442
443    walls = vedo.merge(outler_layer, inner_layer)
444
445
446    boundaries_outer = outler_layer.boundaries()
447    boundaries_inner = inner_layer.boundaries()
448
449    strips = boundaries_outer.join_with_strips(boundaries_inner).triangulate()
450    
451
452    walls = vedo.merge(walls,strips)
453    
454    calculate_features(walls)
455    scatterer_file_name(walls)
456
457    return walls.clean()
458
459def cut_closed_scatterer(scatterer:Mesh,layer_z:float, normals=[(0,0,1)]):
460    origins=[(0,0,layer_z)]
461    closed_scatterer = scatterer.cut_closed_surface(origins=origins, normals=normals)
462    return closed_scatterer
463
464def get_volume(scatterer:Mesh):
465    '''
466    Returns the volume of a mesh
467    '''
468    return scatterer.volume()
def board_name(board: torch.Tensor) -> str:
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)

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:
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))
35    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) -> vedo.mesh.Mesh:
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) -> 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
60        scatterer.metadata["rotX"] = 0
61        scatterer.metadata["rotY"] = 0
62        scatterer.metadata["rotZ"] = 0
63
64        # scatterer.filename = scatterer.filename.split("/")[-1]
65        scatterer.filename = scatterer_file_name(scatterer)
66
67        scatterer.metadata["FILE"] = scatterer.filename.split(".")[0]
68
69
70        rotate(scatterer,(1,0,0),rotx)
71        rotate(scatterer,(0,1,0),roty)
72        rotate(scatterer,(0,0,1),rotz)
73
74        translate(scatterer,dx,dy,dz)
75
76
77    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 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:
 90def load_multiple_scatterers(paths:list[str],  compute_areas:bool = True, compute_normals:bool=True, 
 91                             dxs:list[int]=[],dys:list[int]=[],dzs:list[int]=[], rotxs:list[int]=[], rotys:list[int]=[], rotzs:list[int]=[], root_path:str="") -> Mesh:
 92    '''
 93    Loads multiple scatterers and combines them into a single scatterer object
 94    :param path: The name of the scatterers to load
 95    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default True
 96    :param compute_normals: if true will call `scatterer.compute_normals()`. Default True
 97    :param dxs: List of translations in the x direction to apply to each scatterer
 98    :param dys: List of translations in the y direction to apply to each scatterer
 99    :param dzs: List of translations in the z direction to apply to each scatterer
100    :param rotxs: List pf rotations around the x axis to apply to each scatterer
101    :param rotys: List pf rotations around the y axis to apply to each scatterer
102    :param rotzs: List pf rotations around the z axis to apply to each scatterer
103    :param root_path: The folder containing the file, the scatterer to be loaded will be loaded from `root_path+path`
104    :return: A merged mesh from all of the paths provided
105    '''
106    dxs += [0] * (len(paths) - len(dxs))
107    dys += [0] * (len(paths) - len(dys))
108    dzs += [0] * (len(paths) - len(dzs))
109
110    rotxs += [0] * (len(paths) - len(rotxs))
111    rotys += [0] * (len(paths) - len(rotys))
112    rotzs += [0] * (len(paths) - len(rotzs))
113
114    scatterers = []
115    for i,path in enumerate(paths):
116        scatterer = load_scatterer(path, compute_areas, compute_normals, dxs[i],dys[i],dzs[i],rotxs[i],rotys[i],rotzs[i],root_path)
117        scatterers.append(scatterer)
118    combined = merge_scatterers(*scatterers)
119    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:
121def merge_scatterers(*scatterers:Mesh, flag:bool=False) ->Mesh:
122    '''
123    Combines any number of scatterers into a single scatterer\n
124    :param scatterers: any number of scatterers to combine
125    :param flag: Value will be passed to `vedo.merge`
126    :return: the combined scatterer
127    '''
128    names = []
129    Fnames = []
130    for scatterer in scatterers:
131        names.append(scatterer_file_name(scatterer))
132        Fnames.append(scatterer.metadata["FILE"][0])
133    
134    if flag:
135        combined = vedo.merge(scatterers, flag=True)
136    else:
137        combined = vedo.merge(scatterers)
138    combined.filename = "".join(names)
139    combined.metadata["FILE"] = "".join(Fnames)
140    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:
143def scale_to_diameter(scatterer:Mesh , diameter: float, reset:bool=True, origin:bool=True) -> None:
144    '''
145    Scale a mesh to a given diameter in the x-axis and recomputes normals and areas \n
146    Modifies scatterer in place so does not return anything.\n
147
148    :param scatterer: The scatterer to scale
149    :param diameter: The diameter target
150    '''
151    x1,x2,y1,y2,z1,z2 = scatterer.bounds()
152    diameter_sphere = x2 - x1
153    scatterer.scale(diameter/diameter_sphere,reset=reset, origin=origin)
154    scatterer.compute_cell_size()
155    scatterer.compute_normals()
156    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_plane( scatterer: vedo.mesh.Mesh, origin: tuple[int] = (0, 0, 0), normal: tuple[int] = (1, 0, 0)) -> vedo.mesh.Mesh:
159def get_plane(scatterer: Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> Mesh:
160    '''
161    Get intersection of a scatterer and a plane\n
162    :param scatterer: The scatterer to intersect
163    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
164    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
165    :return: new `Mesh` Containing the intersection of the plane and the scatterer
166    '''
167    intersection = scatterer.clone().intersect_with_plane(origin,normal)
168    intersection.filename = scatterer.filename + "plane" + str(origin)+str(normal)
169    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]:
171def get_lines_from_plane(scatterer:Mesh, origin:tuple[int]=(0,0,0), normal:tuple[int]=(1,0,0)) -> list[int]:
172    '''
173    Gets the edges on a plane from the intersection between a scatterer and the plane\n
174    :param scatterer: The scatterer to intersect
175    :param origin: A point on the plane as a tuple `(x,y,z)`. Default `(0,0,0)`
176    :param normal: The normal to the plane at `point` as a tuple (x,y,z). Default `(1,0,0)`
177    :return: a list of edges in the plane 
178    '''
179
180    mask = [0,0,0]
181    for i in range(3):
182        mask[i] =not normal[i]
183    mask = np.array(mask)
184
185    intersection = get_plane(scatterer, origin, normal)
186    verticies = intersection.vertices
187    lines = intersection.lines
188
189    connections = []
190
191    for i in range(len(lines)):
192        connections.append([verticies[lines[i][0]][mask],verticies[lines[i][1]][mask]])
193
194    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:
196def plot_plane(connections:list[int]) -> None:
197    '''
198    Plot a set of edges assuming they are co-planar\n
199    :param connections: list of connections to plot
200    '''
201    
202    for con in connections:
203        xs = [con[0][0], con[1][0]]
204        ys = [con[0][1], con[1][1]]
205        plt.plot(xs,ys,color = "blue")
206
207    plt.xlim((-0.06,0.06))
208    plt.ylim((-0.06,0.06))
209    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:
211def get_normals_as_points(*scatterers:Mesh, permute_to_points:bool=True) -> Tensor:
212    '''
213    Returns the normal vectors to the surface of a scatterer as a `torch` `Tensor` as acoustools points\n
214    :param scatterers: The scatterer to use
215    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
216    :return: normals
217    '''
218    norm_list = []
219    for scatterer in scatterers:
220        scatterer.compute_normals()
221        norm =  torch.tensor(scatterer.cell_normals).to(device)
222
223        if permute_to_points:
224            norm = torch.permute(norm,(1,0))
225        
226        norm_list.append(norm.to(DTYPE))
227    
228    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:
230def get_centre_of_mass_as_points(*scatterers:Mesh, permute_to_points:bool=True) ->Tensor:
231    '''
232    Returns the centre of mass(es) of a scatterer(s) as a `torch` `Tensor` as acoustools points\n
233    :param scatterers: The scatterer(s) to use
234    :param permute_to_points: If true will permute the order of coordinates to agree with what acoustools expects.
235    :return: centre of mass(es)
236    '''
237    centres_list = []
238    for scatterer in scatterers:
239        centre_of_mass =  torch.tensor(scatterer.center_of_mass()).to(device)
240
241        if permute_to_points:
242            centre_of_mass = torch.unsqueeze(centre_of_mass,1)
243        
244        centres_list.append(centre_of_mass.to(DTYPE))
245    
246    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:
249def get_centres_as_points(*scatterers:Mesh, permute_to_points:bool=True, add_normals:bool=False, normal_scale:float=0.001) ->Tensor:
250    '''
251    Returns the centre of scatterer faces as a `torch` `Tensor` as acoustools points\n
252    :param scatterers: The scatterer to use
253    :param permute_to_points: If `True` will permute the order of coordinates to agree with what acoustools expects.
254    :return: centres
255    '''
256    centre_list = []
257    for scatterer in scatterers:
258        centres =  torch.tensor(scatterer.cell_centers().points).to(device)
259
260        if permute_to_points:
261            centres = torch.permute(centres,(1,0)).unsqueeze_(0)
262        
263        if add_normals:
264            norms= get_normals_as_points(scatterer)
265            centres += norms.real * normal_scale
266        
267        centre_list.append(centres.to(DTYPE))
268    centres = torch.cat(centre_list,dim=0)
269    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_areas(*scatterers: vedo.mesh.Mesh) -> torch.Tensor:
271def get_areas(*scatterers: Mesh) -> Tensor:
272    '''
273    Returns the areas of faces of any number of scatterers\n
274    :param scatterers: The scatterers to use.
275    :return: areas
276    '''
277    area_list = []
278    for scatterer in scatterers:
279        scatterer.compute_cell_size()
280        area_list.append(torch.Tensor(scatterer.celldata["Area"]).to(device))
281    
282    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:
284def get_weight(scatterer:Mesh, density:float=Constants.p_p, g:float=9.81) -> float:
285    '''
286    Get the weight of a scatterer\\
287    :param scatterer: The scatterer to use\\
288    :param density: The density to use. Default density for EPS\\
289    :param g: value for g to use. Default 9.81\\
290    :return: weight
291    '''
292    mass = scatterer.volume() * density
293    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:
295def translate(scatterer:Mesh, dx:float=0,dy:float=0,dz:float=0) -> None:
296    '''
297    Translates a scatterer by (dx,dy,dz) \n
298    Modifies inplace so does not return a value \n
299    :param scatterer: The scatterer to use
300    :param dx: Translation in the x direction
301    :param dy: Translation in the y direction
302    :param dz: Translation in the z direction
303    '''
304    scatterer.shift(np.array([dx,dy,dz]))
305    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):
307def rotate(scatterer:Mesh, axis:tuple[int], rot:float, centre:tuple[int]=(0, 0, 0), rotate_around_COM:bool=False):
308    '''
309    Rotates a scatterer in axis by rot\n
310    Modifies inplace so does not return a value\n
311    :param scatterer: The scatterer to use
312    :param axis: The axis to rotate in
313    :param rot: Angle to rotate in degrees
314    :param centre: point to rotate around
315    :param rotate_around_COM: If True will set `centre` to `scatterer`s centre of mass
316    '''
317    if rotate_around_COM:
318        centre = vedo.vector(get_centre_of_mass_as_points(scatterer).cpu().detach().squeeze())
319
320    if axis[0]:
321        scatterer.metadata["rotX"] = scatterer.metadata["rotX"] + rot
322    if axis[1]:
323        scatterer.metadata["rotY"] = scatterer.metadata["rotY"] + rot
324    if axis[2]:
325        scatterer.metadata["rotZ"] = scatterer.metadata["rotZ"] + rot
326    scatterer.rotate(rot, axis,point=centre)
327    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:
330def 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:
331    '''
332    Downsamples a mesh to have `factor` less elements\n
333    :param scatterer: The scatterer to use
334    :param factor: The factor to downsample by
335    :param n: The desired number of final points, passed to `Vedo.Mesh.decimate`
336    :param method:, `boundaries` - passed to `vedo.decimate`
337    :param compute_areas: if true will call `scatterer.compute_cell_size()`. Default `True`
338    :param compute_normals: if true will call `scatterer.compute_normals()`. Default `True`
339    :return: downsampled mesh
340    '''
341    scatterer_small =  scatterer.decimate(1/factor, n, method, boundaries)
342    
343    scatterer_small.metadata["rotX"] = scatterer.metadata["rotX"]
344    scatterer_small.metadata["rotY"] = scatterer.metadata["rotY"]
345    scatterer_small.metadata["rotZ"] = scatterer.metadata["rotZ"]
346
347    if compute_areas: scatterer_small.compute_cell_size()
348    if compute_normals: 
349        scatterer_small.compute_normals()
350
351    scatterer_small.filename = scatterer_file_name(scatterer_small)  + "-scale-" + str(factor)
352
353
354    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]:
357def centre_scatterer(scatterer:Mesh) -> list[int]:
358    '''
359    Translate scatterer so the centre of mass is at (0,0,0)\n
360    Modifies Mesh in place \n
361    :param scatterer: Scatterer to centre
362    :return: Returns the amount needed to move in each direction
363    '''
364    com = get_centre_of_mass_as_points(scatterer).cpu()
365    correction = [-1*com[:,0].item(), -1*com[:,1].item(), -1*com[:,2].item()]
366    translate(scatterer, dx = correction[0], dy = correction[1], dz=  correction[2])
367
368    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]:
371def get_edge_data(scatterer:Mesh, wavelength:float=Constants.wavelength, print_output:bool=True, break_down_average:bool=False) -> None|tuple[float]:
372    '''
373    Get the maximum, minimum and average size of edges in a mesh. Optionally prints or returns the result.\n
374    :param scatterer: Mesh of interest
375    :param wavelength: Wavenelgth size for printing results as multiple of some wavelength
376    :param print_output: If True, prints results else returns values
377    :break_down_average: If True will also return (distance_sum, N)
378    :return: None if `print_outputs` is `True` else returns `(max_distance, min_distance, average_distance)` and optionally  (distance_sum, N)
379
380    '''
381    points = scatterer.vertices
382
383    distance_sum = 0
384    N = 0
385
386    max_distance = 0
387    min_distance = 100000000
388
389
390    for (start,end) in scatterer.edges:
391        start_point = points[start]
392        end_point = points[end]
393        sqvec = torch.Tensor((start_point-end_point)**2)
394        # print(sqvec, torch.sum(sqvec)**0.5)
395        distance = torch.sum(sqvec)**0.5
396        distance_sum += distance
397        N += 1
398        if distance < min_distance:
399            min_distance = distance
400        if distance > max_distance:
401            max_distance = distance
402
403    average_distance = distance_sum/N
404
405    if print_output:
406        print('Max Distance', max_distance.item(),'=' ,max_distance.item()/wavelength, 'lambda')
407        print('Min Distance', min_distance.item(),'=', min_distance.item()/wavelength, 'lambda')
408        print('Ave Distance', average_distance.item(),'=', average_distance.item()/wavelength, 'lambda')
409    else:
410        if break_down_average:
411            return (max_distance, min_distance, average_distance), (distance_sum, N)
412        else:
413            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:
416def cut_mesh_to_walls(scatterer:Mesh, layer_z:float, layer_normal:tuple[float] = (0,0,-1.0), wall_thickness = 0.001) -> Mesh:
417    '''
418    Cuts a mesh with a given plane and then converts the result to have walls of a certain thickness \n
419    :param scatterer: Mesh to use
420    :param layer_z: coordinate of layer
421    :param layer_normal: Normal to layer (if not +- (0,0,1) then layer_z will not refer to a z coordinate)
422    :param wall_thickness: Thickness of the walls to returns
423    :return: Cut mesh with walls
424    '''
425
426    xmin,xmax, ymin,ymax, zmin,zmax = scatterer.bounds()
427    dx = xmax-xmin
428    dy = ymax-ymin
429
430    scale_x = (dx-2*wall_thickness) / dx
431    scale_y = (dy-2*wall_thickness) / dy
432
433    outler_layer = scatterer.cut_with_plane((0,0,layer_z),layer_normal)
434    inner_layer = outler_layer.clone()
435    inner_layer.scale((scale_x,scale_y,1), origin=False)
436
437    com_outer = get_centre_of_mass_as_points(outler_layer)
438    com_inner = get_centre_of_mass_as_points(inner_layer)
439
440    d_com = (com_outer - com_inner).squeeze()
441
442    translate(inner_layer, *d_com)
443
444    walls = vedo.merge(outler_layer, inner_layer)
445
446
447    boundaries_outer = outler_layer.boundaries()
448    boundaries_inner = inner_layer.boundaries()
449
450    strips = boundaries_outer.join_with_strips(boundaries_inner).triangulate()
451    
452
453    walls = vedo.merge(walls,strips)
454    
455    calculate_features(walls)
456    scatterer_file_name(walls)
457
458    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)]):
460def cut_closed_scatterer(scatterer:Mesh,layer_z:float, normals=[(0,0,1)]):
461    origins=[(0,0,layer_z)]
462    closed_scatterer = scatterer.cut_closed_surface(origins=origins, normals=normals)
463    return closed_scatterer
def get_volume(scatterer: vedo.mesh.Mesh):
465def get_volume(scatterer:Mesh):
466    '''
467    Returns the volume of a mesh
468    '''
469    return scatterer.volume()

Returns the volume of a mesh