Source code for capytaine.meshes.symmetric

"""Special meshes with symmetries, useful to speed up the computations."""
# Copyright (C) 2017-2019 Matthieu Ancellin
# See LICENSE file at <https://github.com/mancellin/capytaine>

import logging
import reprlib
from typing import Union, Callable, Iterable

import numpy as np

from capytaine.meshes.meshes import Mesh
from capytaine.meshes.collections import CollectionOfMeshes
from capytaine.meshes.geometry import Axis, Plane, Oz_axis, inplace_transformation

LOG = logging.getLogger(__name__)


[docs] class SymmetricMesh(CollectionOfMeshes): def __repr__(self): reprer = reprlib.Repr() reprer.maxstring = 90 reprer.maxother = 90 slice_name = reprer.repr(self._meshes[0]) if self.name is not None: return f"{self.__class__.__name__}({slice_name}, name={self.name})" else: return f"{self.__class__.__name__}({slice_name})"
[docs] class ReflectionSymmetricMesh(SymmetricMesh): """A mesh with one vertical symmetry plane. Parameters ---------- half : Mesh or CollectionOfMeshes a mesh describing half of the body plane : Plane the symmetry plane across which the half body is mirrored name :str, optional a name for the mesh """ def __init__(self, half: Union[Mesh, CollectionOfMeshes], plane: Plane, name=None): assert isinstance(half, Mesh) or isinstance(half, CollectionOfMeshes) assert isinstance(plane, Plane) assert plane.normal[2] == 0, "Only vertical reflection planes are supported in ReflectionSymmetry classes." other_half = half.mirrored(plane, name=f"mirrored_of_{half.name}") if name is None: name = f"reflection_of_{half.name}" self.plane = plane.copy() super().__init__((half, other_half), name=name) if self.name is not None: LOG.debug(f"New mirror symmetric mesh: {self.name}.") else: LOG.debug(f"New mirror symmetric mesh.") def __str__(self): return f"{self.__class__.__name__}({self.half.__short_str__()}, plane={self.plane}, name=\"{self.name}\")" def __repr__(self): return f"{self.__class__.__name__}({self.half}, plane={self.plane}, name=\"{self.name}\")" def __rich_repr__(self): yield self.half yield "plane", self.plane yield "name", self.name @property def half(self): return self[0]
[docs] def tree_view(self, fold_symmetry=True, **kwargs): if fold_symmetry: return (self.__short_str__() + '\n' + ' ├─' + self.half.tree_view().replace('\n', '\n │ ') + '\n' + f" └─mirrored copy of the above {self.half.__short_str__()}") else: return CollectionOfMeshes.tree_view(self, **kwargs)
def __deepcopy__(self, *args): return ReflectionSymmetricMesh(self.half.copy(), self.plane, name=self.name)
[docs] def join_meshes(*meshes, name=None): assert all(isinstance(mesh, ReflectionSymmetricMesh) for mesh in meshes), \ "Only meshes with the same symmetry can be joined together." assert all(meshes[0].plane == mesh.plane for mesh in meshes), \ "Only reflection symmetric meshes with the same reflection plane can be joined together." half_mesh = CollectionOfMeshes([mesh.half for mesh in meshes], name=f"half_of_{name}" if name is not None else None) return ReflectionSymmetricMesh(half_mesh, plane=meshes[0].plane, name=name)
@inplace_transformation def translate(self, vector): self.plane.translate(vector) CollectionOfMeshes.translate(self, vector) return self @inplace_transformation def rotate(self, axis: Axis, angle: float): self.plane.rotate(axis, angle) CollectionOfMeshes.rotate(self, axis, angle) return self @inplace_transformation def mirror(self, plane: Plane): self.plane.mirror(plane) CollectionOfMeshes.mirror(self, plane) return self
[docs] class TranslationalSymmetricMesh(SymmetricMesh): """A mesh with a repeating pattern by translation. Parameters ---------- mesh_slice : Mesh or CollectionOfMeshes the pattern that will be repeated to form the whole body translation : array(3) the vector of the translation nb_repetitions : int, optional the number of repetitions of the pattern (excluding the original one, default: 1) name : str, optional a name for the mesh """ def __init__(self, mesh_slice: Union[Mesh, CollectionOfMeshes], translation, nb_repetitions=1, name=None): assert isinstance(mesh_slice, Mesh) or isinstance(mesh_slice, CollectionOfMeshes) assert isinstance(nb_repetitions, int) assert nb_repetitions >= 1 translation = np.asarray(translation).copy() assert translation.shape == (3,) assert translation[2] == 0 # Only horizontal translation are supported. slices = [mesh_slice] for i in range(1, nb_repetitions+1): slices.append(mesh_slice.translated(vector=i*translation, name=f"repetition_{i}_of_{mesh_slice.name}")) if name is None: name = f"translation_of_{mesh_slice.name}" self.translation = translation super().__init__(slices, name=name) if self.name is not None: LOG.debug(f"New translation symmetric mesh: {self.name}.") else: LOG.debug(f"New translation symmetric mesh.") @property def first_slice(self): return self[0] def __str__(self): return f"{self.__class__.__name__}({self.first_slice.__short_str__()}, translation={self.translation}, nb_repetitions={len(self)-1}, name=\"{self.name}\")" def __repr__(self): return f"{self.__class__.__name__}({self.first_slice}, translation={self.translation}, nb_repetitions={len(self)-1}, name=\"{self.name}\")" def __rich_repr__(self): yield self.first_slice yield "translation", self.translation yield "nb_repetitions", len(self)-1 yield "name", self.name
[docs] def tree_view(self, fold_symmetry=True, **kwargs): if fold_symmetry: return (self.__short_str__() + '\n' + ' ├─' + self.first_slice.tree_view().replace('\n', '\n │ ') + '\n' + f" └─{len(self)-1} translated copies of the above {self.first_slice.__short_str__()}") else: return CollectionOfMeshes.tree_view(self, **kwargs)
def __deepcopy__(self, *args): return TranslationalSymmetricMesh(self.first_slice.copy(), self.translation, nb_repetitions=len(self) - 1, name=self.name) @inplace_transformation def translate(self, vector): CollectionOfMeshes.translate(self, vector) return self @inplace_transformation def rotate(self, axis: Axis, angle: float): self.translation = axis.rotate_vectors([self.translation], angle)[0, :] CollectionOfMeshes.rotate(self, axis, angle) return self @inplace_transformation def mirror(self, plane: Plane): self.translation -= 2 * (self.translation @ plane.normal) * plane.normal CollectionOfMeshes.mirror(self, plane) return self
[docs] def join_meshes(*meshes, name=None): assert all(isinstance(mesh, TranslationalSymmetricMesh) for mesh in meshes), \ "Only meshes with the same symmetry can be joined together." assert all(np.allclose(meshes[0].translation, mesh.translation) for mesh in meshes), \ "Only translation symmetric meshes with the same translation vector can be joined together." assert all(len(meshes[0]) == len(mesh) for mesh in meshes), \ "Only symmetric meshes with the same number of elements can be joined together." mesh_strip = CollectionOfMeshes([mesh.first_slice for mesh in meshes], name=f"strip_of_{name}" if name is not None else None) return TranslationalSymmetricMesh(mesh_strip, translation=meshes[0].translation, nb_repetitions=len(meshes[0]) - 1, name=name)
[docs] def build_regular_array_of_meshes(base_mesh, distance, nb_bodies): """Create an array of objects using TranslationalSymmetries. Parameters ---------- base_mesh : Mesh or CollectionOfMeshes or SymmetricMesh The mesh to duplicate to create the array distance : float Center-to-center distance between objects in the array nb_bodies : couple of ints Number of objects in the x and y directions. Returns ------- TranslationalSymmetricMesh """ if nb_bodies[0] == 1: line = base_mesh else: line = TranslationalSymmetricMesh(base_mesh, translation=(distance, 0.0, 0.0), nb_repetitions=nb_bodies[0] - 1, name=f'line_of_{base_mesh.name}') if nb_bodies[1] == 1: array = line else: array = TranslationalSymmetricMesh(line, translation=(0.0, distance, 0.0), nb_repetitions=nb_bodies[1] - 1, name=f'array_of_{base_mesh.name}') return array
[docs] class AxialSymmetricMesh(SymmetricMesh): """A mesh with a repeating pattern by rotation. Parameters ---------- mesh_slice : Mesh or CollectionOfMeshes the pattern that will be repeated to form the whole body axis : Axis, optional symmetry axis nb_repetitions : int, optional the number of repetitions of the pattern (excluding the original one, default: 1) name : str, optional a name for the mesh """ def __init__(self, mesh_slice: Union[Mesh, CollectionOfMeshes], axis: Axis=Oz_axis, nb_repetitions: int=1, name=None): assert isinstance(mesh_slice, Mesh) or isinstance(mesh_slice, CollectionOfMeshes) assert isinstance(nb_repetitions, int) assert nb_repetitions >= 1 assert isinstance(axis, Axis) slices = [mesh_slice] for i in range(1, nb_repetitions+1): slices.append(mesh_slice.rotated(axis, angle=2*i*np.pi/(nb_repetitions+1), name=f"rotation_{i}_of_{mesh_slice.name}")) if name is None: name = f"rotation_of_{mesh_slice.name}" self.axis = axis.copy() super().__init__(slices, name=name) if not axis.is_parallel_to(Oz_axis): LOG.warning(f"{self.name} is an axi-symmetric mesh along a non vertical axis.") if self.name is not None: LOG.debug(f"New rotation symmetric mesh: {self.name}.") else: LOG.debug(f"New rotation symmetric mesh.")
[docs] @staticmethod def from_profile(profile: Union[Callable, Iterable[float]], z_range: Iterable[float]=np.linspace(-5, 0, 20), axis: Axis=Oz_axis, nphi: int=20, name=None): """Return a floating body using the axial symmetry. The shape of the body can be defined either with a function defining the profile as [f(z), 0, z] for z in z_range. Alternatively, the profile can be defined as a list of points. The number of vertices along the vertical direction is len(z_range) in the first case and profile.shape[0] in the second case. Parameters ---------- profile : function(float → float) or array(N, 3) define the shape of the body either as a function or a list of points. z_range: array(N), optional used only if the profile is defined as a function. axis : Axis symmetry axis nphi : int, optional number of vertical slices forming the body name : str, optional name of the generated body (optional) Returns ------- AxialSymmetricMesh the generated mesh """ if name is None: name = "axisymmetric_mesh" if callable(profile): z_range = np.asarray(z_range) x_values = [profile(z) for z in z_range] profile_array = np.stack([x_values, np.zeros(len(z_range)), z_range]).T else: profile_array = np.asarray(profile) assert len(profile_array.shape) == 2 assert profile_array.shape[1] == 3 n = profile_array.shape[0] angle = 2 * np.pi / nphi nodes_slice = np.concatenate([profile_array, axis.rotate_points(profile_array, angle)]) faces_slice = np.array([[i, i+n, i+n+1, i+1] for i in range(n-1)]) body_slice = Mesh(nodes_slice, faces_slice, name=f"slice_of_{name}") body_slice.merge_duplicates() body_slice.heal_triangles() return AxialSymmetricMesh(body_slice, axis=axis, nb_repetitions=nphi - 1, name=name)
@property def first_slice(self): return self[0] def __str__(self): return f"{self.__class__.__name__}({self.first_slice.__short_str__()}, axis={self.axis}, nb_repetitions={len(self)-1}, name=\"{self.name}\")" def __repr__(self): return f"{self.__class__.__name__}({self.first_slice}, axis={self.axis}, nb_repetitions={len(self)-1}, name=\"{self.name}\")" def __rich_repr__(self): yield self.first_slice yield "axis", self.axis yield "nb_repetitions", len(self)-1 yield "name", self.name
[docs] def tree_view(self, fold_symmetry=True, **kwargs): if fold_symmetry: return (self.__short_str__() + '\n' + ' ├─' + self.first_slice.tree_view().replace('\n', '\n │ ') + '\n' + f" └─{len(self)-1} rotated copies of the above {self.first_slice.__short_str__()}") else: return CollectionOfMeshes.tree_view(self, **kwargs)
def __deepcopy__(self, *args): return AxialSymmetricMesh(self.first_slice.copy(), axis=self.axis.copy(), nb_repetitions=len(self) - 1, name=self.name)
[docs] def join_meshes(*meshes, name=None): assert all(isinstance(mesh, AxialSymmetricMesh) for mesh in meshes), \ "Only meshes with the same symmetry can be joined together." assert all(meshes[0].axis == mesh.axis for mesh in meshes), \ "Only axisymmetric meshes with the same symmetry axis can be joined together." assert all(len(meshes[0]) == len(mesh) for mesh in meshes), \ "Only axisymmetric meshes with the same number of elements can be joined together." mesh_slice = CollectionOfMeshes([mesh.first_slice for mesh in meshes], name=f"slice_of_{name}" if name is not None else None) return AxialSymmetricMesh(mesh_slice, axis=meshes[0].axis, nb_repetitions=len(meshes[0]) - 1, name=name)
@inplace_transformation def translate(self, vector): self.axis.translate(vector) CollectionOfMeshes.translate(self, vector) return self @inplace_transformation def rotate(self, other_axis: Axis, angle: float): self.axis.rotate(other_axis, angle) CollectionOfMeshes.rotate(self, other_axis, angle) return self @inplace_transformation def mirror(self, plane: Plane): self.axis.mirror(plane) CollectionOfMeshes.mirror(self, plane) return self