"""All classes related to SampleMesh."""
import trimesh
import os
from enum import Enum
import mesh2scattering.input.bc as bc
[docs]
class SurfaceType(Enum):
"""Defines the type of a sample mesh.
Can be a trimesh object or a path to a stl file.
"""
PERIODIC = "Periodic"
"""The surface is periodic."""
STOCHASTIC = "Stochastic"
"""The surface is stochastic."""
FLAT = "Flat"
"""The surface is flat, basically for the reference surface."""
[docs]
class SampleShape(Enum):
"""Defines the shape of a sample mesh.
Can be round or square.
"""
ROUND = "Round"
"""The sample shape is round."""
SQUARE = "Square"
"""The sample shape is square."""
[docs]
class SurfaceDescription():
"""Initializes the SurfaceDescription object.
Parameters
----------
structural_wavelength_x : float, optional
structural wavelength in x direction, by default 0.
structural_wavelength_y : float, optional
structural wavelength in y direction, by default 0.
surface_type : SurfaceType, optional
surface type, by default SurfaceType.PERIODIC.
model_scale : float, optional
model scale, by default 1.
symmetry_azimuth : list, optional
azimuth symmetry, by default [].
symmetry_rotational : bool, optional
rotational symmetry, by default False.
comment : str, optional
comment, by default "".
"""
_structural_wavelength_x: float = 0
_structural_wavelength_y: float = 0
_model_scale: float = 1
_symmetry_azimuth: list = []
_symmetry_rotational: bool = False
_surface_type: SurfaceType = SurfaceType.PERIODIC
_comment: str = ""
_structural_depth: float = 0
def __init__(
self,
structural_wavelength_x: float=0,
structural_wavelength_y: float=0,
structural_depth: float=0,
surface_type: SurfaceType=SurfaceType.PERIODIC,
model_scale: float=1,
symmetry_azimuth: list=None,
symmetry_rotational: bool=False,
comment: str="") -> None:
"""Initialize the SurfaceDescription object.
Parameters
----------
structural_wavelength_x : float, optional
structural wavelength in x direction, by default 0.
structural_wavelength_y : float, optional
structural wavelength in y direction, by default 0.
structural_depth : float, optional
structural depth in meters, by default 0.
surface_type : SurfaceType, optional
surface type, by default SurfaceType.PERIODIC.
model_scale : float, optional
model scale, by default 1.
symmetry_azimuth : list, optional
along which azimuth angles in degree is the surface symmetric,
by default [].
symmetry_rotational : bool, optional
rotational symmetry, by default False.
comment : str, optional
comment, by default "".
Returns
-------
SurfaceDescription
surface description object.
"""
if symmetry_azimuth is None:
symmetry_azimuth = []
if not isinstance(structural_wavelength_x, (int, float)) or \
structural_wavelength_x < 0:
raise ValueError(
"structural_wavelength_x must be a float and >= 0.")
if not isinstance(structural_wavelength_y, (int, float)) or \
structural_wavelength_y < 0:
raise ValueError(
"structural_wavelength_y must be a float and >= 0.")
if not isinstance(model_scale, (int, float)) or model_scale <= 0:
raise ValueError("model_scale must be a float and > 0.")
if not isinstance(symmetry_azimuth, list):
raise ValueError("symmetry_azimuth must be a list.")
for angle in symmetry_azimuth:
if not isinstance(angle, (int, float)) or angle < 0 or angle > 360:
raise ValueError(
"elements of symmetry_azimuth must be a number between "
"0° and 360°.")
if not isinstance(symmetry_rotational, bool):
raise ValueError("symmetry_rotational must be a bool.")
if not isinstance(comment, str):
raise ValueError("comment must be a string.")
if not isinstance(surface_type, SurfaceType):
raise ValueError("surface_type must be a SurfaceType.")
if not isinstance(structural_depth, (int, float)) or \
structural_depth < 0:
raise ValueError("structural_depth must be a float and >= 0.")
self._structural_wavelength_x = structural_wavelength_x
self._structural_wavelength_y = structural_wavelength_y
self._model_scale = model_scale
self._symmetry_azimuth = symmetry_azimuth
self._symmetry_rotational = symmetry_rotational
self._comment = comment
self._surface_type = surface_type
self._structural_depth = structural_depth
@property
def structural_wavelength_x(self):
"""Defines the structural wavelength in x direction.
Returns
-------
float
The structural wavelength in x direction.
"""
return self._structural_wavelength_x
@property
def structural_wavelength_y(self):
"""Defines the structural wavelength in y direction.
Returns
-------
float
The structural wavelength in y direction.
"""
return self._structural_wavelength_y
@property
def structural_depth(self):
"""Defines the structural depth.
Returns
-------
float
The structural depth.
"""
return self._structural_depth
@property
def surface_type(self):
"""Defines the surface type.
Returns
-------
SurfaceType
The surface type.
"""
return self._surface_type
@property
def model_scale(self):
"""Defines the model scale.
Returns
-------
float
The model scale.
"""
return self._model_scale
@property
def symmetry_azimuth(self):
"""Defines the azimuth symmetry.
Returns
-------
list
The azimuth symmetry.
"""
return self._symmetry_azimuth
@property
def symmetry_rotational(self):
"""Defines the rotational symmetry.
Returns
-------
bool
The rotational symmetry.
"""
return self._symmetry_rotational
@property
def comment(self):
"""Defines the comment.
Returns
-------
str
The comment.
"""
return self._comment
[docs]
class SampleMesh():
"""Initializes the SampleMesh object.
Parameters
----------
mesh : trimesh.Trimesh
trimesh object representing the sample mesh.
surface_description : SurfaceDescription
surface description of the sample mesh.
sample_diameter : float, optional
diameter of the sample, by default 0.8
sample_shape : str, optional
shape of the sample, by default 'round'
"""
_mesh:trimesh.Trimesh = None
_surface_description: SurfaceDescription = None
_sample_diameter: float = 0.8
_sample_shape: SampleShape = SampleShape.ROUND
_n_repetitions_x: int = 0
_n_repetitions_y: int = 0
def __init__(
self, mesh: trimesh.Trimesh,
surface_description: SurfaceDescription,
sample_baseplate_hight: float=.01,
sample_diameter: float=0.8,
sample_shape: SampleShape=SampleShape.ROUND,
bc_mapping: bc.BoundaryConditionMapping=None,
) -> None:
"""Initialize the SampleMesh object.
Parameters
----------
mesh : trimesh.Trimesh
trimesh object representing the sample mesh.
surface_description : SurfaceDescription
surface description of the sample mesh.
sample_baseplate_hight : float, optional
height of the baseplate, by default 0
sample_diameter : float, optional
diameter of the sample, by default 0.8
sample_shape : str, optional
shape of the sample, by default 'round'
bc_mapping : BoundaryConditionMapping, None
boundary condition mapping for the sample mesh, by default None,
which means that the mesh surface is sound hard.
Returns
-------
SampleMesh
sample mesh object.
"""
if not isinstance(mesh, trimesh.Trimesh):
raise ValueError("mesh must be a trimesh.Trimesh object.")
if not isinstance(
sample_baseplate_hight, (int, float)) or \
sample_baseplate_hight <= 0:
raise ValueError(
"sample_baseplate_hight must be a float or int and >0.")
if not isinstance(
sample_diameter, (int, float)) or sample_diameter <= 0:
raise ValueError("sample_diameter must be a float or int and >0.")
if not isinstance(sample_shape, SampleShape):
raise ValueError("sample_shape must be a SampleShape.")
if not isinstance(surface_description, SurfaceDescription):
raise ValueError(
"surface_description must be a SurfaceDescription object.")
if bc_mapping is not None:
if not isinstance(
bc_mapping, bc.BoundaryConditionMapping):
raise ValueError(
"bc_mapping must be a "
"BoundaryConditionMapping object or None.")
# check if BoundaryConditionMapping matches the mesh properties
if bc_mapping is not None:
if bc_mapping.n_mesh_faces != mesh.faces.shape[0]:
raise ValueError(
"The number of elements in the BoundaryConditionMapping "
"must match the number of faces in the mesh.")
self._mesh = mesh
self._surface_description = surface_description
self._sample_diameter = sample_diameter
self._sample_shape = sample_shape
self._sample_baseplate_hight = sample_baseplate_hight
self._bc_mapping = bc_mapping
# calculate Number of repetitions in x and y direction
Lambda_x = surface_description.structural_wavelength_x
Lambda_y = surface_description.structural_wavelength_y
self._n_repetitions_x = (
sample_diameter / Lambda_x) if Lambda_x > 0 else 0
self._n_repetitions_y = (
sample_diameter / Lambda_y) if Lambda_y > 0 else 0
@property
def bc_mapping(self):
"""Get the boundary condition mapping of the sample mesh.
Returns
-------
bc_mapping : BoundaryConditionMapping, None
The boundary condition mapping of the sample mesh.
"""
return self._bc_mapping
@property
def sample_baseplate_hight(self):
"""Defines the height of the baseplate.
Returns
-------
float
The height of the baseplate.
"""
return self._sample_baseplate_hight
@property
def mesh(self):
"""Defines the sample mesh.
Returns
-------
trimesh.Trimesh
The sample mesh.
"""
return self._mesh
@property
def surface_description(self):
"""Defines the surface description.
Returns
-------
SurfaceDescription
The surface description.
"""
return self._surface_description
@property
def sample_diameter(self):
"""Defines the diameter of the sample.
Returns
-------
float
The diameter of the sample.
"""
return self._sample_diameter
@property
def sample_shape(self):
"""Defines the shape of the sample.
Returns
-------
SampleShape
The shape of the sample.
"""
return self._sample_shape
@property
def mesh_faces(self):
"""Defines the faces of the mesh.
Returns
-------
numpy.ndarray
The faces of the mesh.
"""
return self._mesh.faces
@property
def mesh_vertices(self):
"""Defines the vertices of the mesh.
Returns
-------
numpy.ndarray
The vertices of the mesh.
"""
return self._mesh.vertices
@property
def n_mesh_elements(self):
"""Number of mesh elements/faces.
Returns
-------
int
number of mesh elements/faces.
"""
return self.mesh_faces.shape[0]
@property
def n_mesh_nodes(self):
"""Number of mesh nodes/vertices.
Returns
-------
int
number of mesh nodes/vertices.
"""
return self.mesh_vertices.shape[0]
@property
def n_repetitions_x(self):
"""Defines the number of repetitions in x direction.
Returns
-------
int
The number of repetitions in x direction.
"""
return self._n_repetitions_x
@property
def n_repetitions_y(self):
"""Defines the number of repetitions in y direction.
Returns
-------
int
The number of repetitions in y direction.
"""
return self._n_repetitions_y
[docs]
def export_numcalc(self, folder_path, start=200000):
"""
Write mesh to NumCalc input format.
NumCalc meshes consist of two text files Nodes.txt and Elements.txt.
The Nodes.txt file contains the coordinates of the vertices and the
Elements.txt file contains the indices of the vertices that form the
faces of the mesh.
Parameters
----------
folder_path : str
Path to the directory where the mesh is saved. The mesh is saved in
'Nodes.txt' and 'Elements.txt' files.
start : int, optional
The nodes and elements of the mesh are numbered and the first
element will have the number `start`. In NumCalc, each Node
must have a unique number. The nodes/elements of the mesh for
which the HRTFs are simulated start at 1. Thus `start` must
at least be greater than the number of nodes/elements in the mesh.
"""
vertices = self.mesh_vertices
faces = self.mesh_faces
# check output directory
if not os.path.isdir(folder_path):
os.mkdir(folder_path)
# write nodes
N = int(self.mesh.vertices.shape[0])
start = int(start)
nodes = f"{N}\n"
for nn in range(N):
nodes += (f"{int(start + nn)} "
f"{vertices[nn, 0]} "
f"{vertices[nn, 1]} "
f"{vertices[nn, 2]}\n")
with open(os.path.join(folder_path, "Nodes.txt"), "w") as f_id:
f_id.write(nodes)
# write elements
N = int(faces.shape[0])
elements = f"{N}\n"
for nn in range(N):
elements += (
f"{int(start + nn)} "
f"{faces[nn, 0] + start} "
f"{faces[nn, 1] + start} "
f"{faces[nn, 2] + start} "
"0 0 0\n")
with open(os.path.join(folder_path, "Elements.txt"), "w") as f_id:
f_id.write(elements)