Source code for EasyFEA.geoms._geom

# Copyright (C) 2021-2025 Université Gustave Eiffel.
# This file is part of the EasyFEA project.
# EasyFEA is distributed under the terms of the GNU General Public License v3, see LICENSE.txt and CREDITS.md for more information.

"""Module containing the _Geom class used to construct geometry. Geometry inherits from _Geom."""

from abc import ABC, abstractmethod
import numpy as np
import copy

from ._utils import Point, Rotate, Symmetry

from ..fem._utils import ElemType
from typing import Union, Optional, Iterable, TYPE_CHECKING
from ..utilities import _types, _params

if TYPE_CHECKING:
    from ..geoms import Point, Line, Circle, CircleArc, Points, Contour, Domain

    ContourCompatible = Union[Line, CircleArc, Points]
    CrackCompatible = Union[Line, Points, Contour, CircleArc]
    RefineCompatible = Union[Point, Circle, str]


[docs] class _Geom(ABC): """Geometric class.""" GeomCompatible = Union["_Geom", "Domain", "Circle", "Points", "Contour"] meshSize: float = _params.PositiveScalarParameter() """Element size used for meshing.""" name: str = _params.StringParameter() """Name of the geometric object.""" isHollow: bool = _params.BoolParameter() """Indicates whether the formed geometry is hollow.""" isOpen: bool = _params.BoolParameter() """Indicates whether the geometry is open, typically to model cracks.""" def __init__( self, points: list[Point], meshSize: _types.Number, name: str, isHollow: bool, isOpen: bool, ): """Creates a geometric object. Parameters ---------- points : list[Point] list of points to build the geometric object meshSize : _types.Number mesh size that will be used to create the mesh >= 0 name : str object name isHollow : bool Indicates whether the formed geometry is hollow (i.e., contains voids). isOpen : bool Indicates whether the geometry is open, for instance to represent a crack. """ assert isinstance(points, Iterable) and isinstance( points[0], Point ), "points must be a list of points." self.__points: list[Point] = points self.meshSize = meshSize self.name = name self.isHollow = isHollow self.isOpen = isOpen # points doesn't have a public setter for safety @property def points(self) -> list[Point]: """The list of points used to build the geometric object.""" return self.__points @property def coord(self) -> _types.FloatArray: """Returns the coordinates of all points as a NumPy array.""" return np.asarray([p.coord for p in self.points], dtype=float)
[docs] @abstractmethod def Get_coord_for_plot(self) -> tuple[_types.FloatArray, _types.FloatArray]: """Returns lines and points coordinates for plotting. Returns ------- tuple of ndarray Lines and points coordinates as NumPy arrays. """ lines = self.coord points = lines[[0, -1]] return lines, points
[docs] def copy(self): new = copy.deepcopy(self) new.name = new.name + "_copy" return new
[docs] def Translate(self, dx: float = 0.0, dy: float = 0.0, dz: float = 0.0) -> None: """Translates the geometry in 3D space. Parameters ---------- dx : float, optional Translation along the x-axis, by default 0.0. dy : float, optional Translation along the y-axis, by default 0.0. dz : float, optional Translation along the z-axis, by default 0.0. """ # to translate an object, all you have to do is move these points [p.Translate(dx, dy, dz) for p in self.__points] # type: ignore [func-returns-value]
[docs] def Rotate( self, theta: float, center: tuple = (0, 0, 0), direction: tuple = (0, 0, 1) ) -> None: """Rotates the object coordinates around an axis. Parameters ---------- theta : float rotation angle in [deg] center : tuple, optional rotation center, by default (0,0,0) direction : tuple, optional rotation direction, by default (0,0,1) """ oldCoord = self.coord newCoord = Rotate(oldCoord, theta, center, direction) dec = newCoord - oldCoord [point.Translate(*dec[p]) for p, point in enumerate(self.points)] # type: ignore [func-returns-value]
[docs] def Symmetry( self, point: _types.Coords = (0, 0, 0), n: _types.Coords = (1, 0, 0) ) -> None: """Reflects the geometry with respect to a plane. Parameters ---------- point : Coords, optional A point on the reflection plane, by default (0, 0, 0). n : Coords, optional Normal vector of the plane, by default (1, 0, 0). """ oldCoord = self.coord newCoord = Symmetry(oldCoord, point, n) dec = newCoord - oldCoord [point.Translate(*dec[p]) for p, point in enumerate(self.points)] # type: ignore [func-returns-value]
[docs] def Mesh_2D( self, inclusions: list[GeomCompatible] = [], elemType: ElemType = ElemType.TRI3, cracks: list["CrackCompatible"] = [], refineGeoms: list["RefineCompatible"] = [], isOrganised=False, additionalSurfaces: list[GeomCompatible] = [], additionalLines: list[Union["Line", "CircleArc"]] = [], additionalPoints: list["Point"] = [], folder="", ): """Creates a 2D mesh from a contour and inclusions that must form a closed plane surface. Parameters ---------- inclusions : list[Domain, Circle, Points, Contour], optional list of hollow and filled geom objects inside the domain elemType : ElemType, optional element type, by default "TRI3" ["TRI3", "TRI6", "TRI10", "TRI15", "QUAD4", "QUAD8", "QUAD9"] cracks : list[Line | Points | Contour | CircleArc] list of geom object used to create open or closed cracks refineGeoms : list[Domain|Circle|str], optional list of geom object for mesh refinement, by default [] isOrganised : bool, optional mesh is organized, by default False additionalSurfaces : list[Domain, Circle, Points, Contour] additional surfaces that will be added to or removed from the surfaces created by the contour and the inclusions. (e.g Domain, Circle, Contour, Points). Tip: if the mesh is not well generated, you can also give the inclusions. additionalLines : list[Union[Line,CircleArc]] additional lines that will be added to the surfaces created by the contour and the inclusions. (e.g Domain, Circle, Contour, Points). WARNING: lines must be within the domain. additionalPoints : list[Point] additional points that will be added to the surfaces created by the contour and the inclusions. WARNING: points must be within the domain. folder : str, optional default mesh.msh folder, by default "" does not save the mesh Returns ------- Mesh Created mesh """ from ..fem._gmsh_interface import Mesher mesher = Mesher() mesh = mesher.Mesh_2D( self, inclusions=inclusions, elemType=elemType, cracks=cracks, refineGeoms=refineGeoms, isOrganised=isOrganised, additionalSurfaces=additionalSurfaces, additionalLines=additionalLines, additionalPoints=additionalPoints, folder=folder, ) return mesh
[docs] def Mesh_Extrude( self, inclusions: list[GeomCompatible] = [], extrude: _types.Coords = (0, 0, 1), layers: list[int] = [], elemType: ElemType = ElemType.TETRA4, cracks: list["CrackCompatible"] = [], refineGeoms: list["RefineCompatible"] = [], isOrganised=False, additionalSurfaces: list[GeomCompatible] = [], additionalLines: list[Union["Line", "CircleArc"]] = [], additionalPoints: list["Point"] = [], folder="", ): """Creates a 3D mesh by extruding a surface constructed from a contour and inclusions. Parameters ---------- inclusions : list[Domain, Circle, Points, Contour], optional list of hollow and filled geom objects inside the domain extrude : Coords, optional extrusion vector, by default [0,0,1] layers: list[int], optional layers in the extrusion, by default [] elemType : ElemType, optional element type, by default "TETRA4" ["TETRA4", "TETRA10", "HEXA8", "HEXA20", "HEXA27", "PRISM6", "PRISM15", "PRISM18"] cracks : list[Line | Points | Contour | CircleArc] list of geom object used to create open or closed cracks refineGeoms : list[Domain|Circle|str], optional list of geom object for mesh refinement, by default [] isOrganised : bool, optional mesh is organized, by default False additionalSurfaces : list[Domain, Circle, Points, Contour] additional surfaces that will be added to or removed from the surfaces created by the contour and the inclusions. (e.g Domain, Circle, Contour, Points). Tip: if the mesh is not well generated, you can also give the inclusions. additionalLines : list[Union[Line,CircleArc]] additional lines that will be added to the surfaces created by the contour and the inclusions. (e.g Domain, Circle, Contour, Points). WARNING: lines must be within the domain. additionalPoints : list[Point] additional points that will be added to the surfaces created by the contour and the inclusions. WARNING: points must be within the domain. folder : str, optional default mesh.msh folder, by default "" does not save the mesh Returns ------- Mesh Created mesh """ from ..fem._gmsh_interface import Mesher mesher = Mesher() mesh = mesher.Mesh_Extrude( self, inclusions=inclusions, extrude=extrude, layers=layers, elemType=elemType, cracks=cracks, refineGeoms=refineGeoms, isOrganised=isOrganised, additionalSurfaces=additionalSurfaces, additionalLines=additionalLines, additionalPoints=additionalPoints, folder=folder, ) return mesh
[docs] def Mesh_Revolve( self, inclusions: list[GeomCompatible] = [], axis: Optional["Line"] = None, angle=360, layers: list[int] = [30], elemType: ElemType = ElemType.TETRA4, cracks: list["CrackCompatible"] = [], refineGeoms: list["RefineCompatible"] = [], isOrganised=False, additionalSurfaces: list[GeomCompatible] = [], additionalLines: list[Union["Line", "CircleArc"]] = [], additionalPoints: list["Point"] = [], folder="", ): """Creates a 3D mesh by rotating a surface along an axis. Parameters ---------- inclusions : list[Domain, Circle, Points, Contour], optional list of hollow and filled geom objects inside the domain axis : Line, optional revolution axis, by default Line((0, 0), (0, 1)) angle : float|int, optional revolution angle in [deg], by default 360 layers: list[int], optional layers in extrusion, by default [30] elemType : ElemType, optional element type, by default "TETRA4" ["TETRA4", "TETRA10", "HEXA8", "HEXA20", "HEXA27", "PRISM6", "PRISM15", "PRISM18"] cracks : list[Line | Points | Contour | CircleArc] list of geom object used to create open or closed cracks refineGeoms : list[Domain|Circle|str], optional list of geom object for mesh refinement, by default [] isOrganised : bool, optional mesh is organized, by default False additionalSurfaces : list[Domain, Circle, Points, Contour] additional surfaces that will be added to or removed from the surfaces created by the contour and the inclusions. (e.g Domain, Circle, Contour, Points). Tip: if the mesh is not well generated, you can also give the inclusions. additionalLines : list[Union[Line,CircleArc]] additional lines that will be added to the surfaces created by the contour and the inclusions. (e.g Domain, Circle, Contour, Points). WARNING: lines must be within the domain. additionalPoints : list[Point] additional points that will be added to the surfaces created by the contour and the inclusions. WARNING: points must be within the domain. folder : str, optional default mesh.msh folder, by default "" does not save the mesh Returns ------- Mesh Created mesh """ from ..fem._gmsh_interface import Mesher if axis is None: from ..geoms import Line axis = Line((0, 0), (0, 1)) mesher = Mesher() mesh = mesher.Mesh_Revolve( self, inclusions=inclusions, axis=axis, angle=angle, layers=layers, elemType=elemType, cracks=cracks, refineGeoms=refineGeoms, isOrganised=isOrganised, additionalSurfaces=additionalSurfaces, additionalLines=additionalLines, additionalPoints=additionalPoints, folder=folder, ) return mesh
[docs] def Plot( self, ax: Optional[_types.Axes] = None, color: str = "", name: str = "", lw: Optional[_types.Number] = None, ls: Optional[str] = None, plotPoints: bool = True, ) -> _types.Axes: """Plots the geometry using Matplotlib. Parameters ---------- ax : matplotlib axis, optional Axis to plot on. If None, a new one is created. color : str, optional Line color. name : str, optional Label for the object. lw : float, optional Line width. ls : str, optional Line style. plotPoints : bool, optional If True, display the object's defining points. Returns ------- Axes The axis with the plotted geometry. """ from ..utilities.Display import Init_Axes, _Axis_equal_3D lines, points = self.Get_coord_for_plot() if ax is None: dimMax = 2 if np.abs(lines[:, 2].max()) == 0 else 3 ax = Init_Axes(dimMax) ax.grid() inDim = 3 if ax.name == "3d" else 2 name = self.name if name == "" else name if color != "": ax.plot(*lines[:, :inDim].T, color=color, label=name, lw=lw, ls=ls) else: ax.plot(*lines[:, :inDim].T, label=name, lw=lw, ls=ls) if plotPoints: ax.plot(*points[:, :inDim].T, ls="", marker=".", c="black") if inDim == 3: xlim, ylim, zlim = ax.get_xlim(), ax.get_ylim(), ax.get_zlim() # type: ignore [union-attr] oldBounds = np.array([xlim, ylim, zlim]).T lines = np.concatenate((lines, oldBounds), 0) _Axis_equal_3D(ax, lines) # type: ignore else: ax.axis("equal") return ax
[docs] @staticmethod def Plot_Geoms( geoms: list["_Geom"], ax: Optional[_types.Axes] = None, color: str = "", name: str = "", plotPoints: bool = True, plotLegend: bool = True, ) -> _types.Axes: """Plots a list of geometric objects on the same axis. Parameters ---------- geoms : list of _Geom Geometries to plot. ax : matplotlib axis, optional Axis to use. If None, a new one is created. color : str, optional Line color. name : str, optional Label for the geometries. plotPoints : bool, optional Whether to plot defining points. plotLegend : bool, optional Whether to display the legend. Returns ------- Axes The axis with the plotted geometries. """ for g, geom in enumerate(geoms): if isinstance(geom, Point): continue if ax is None: ax = geom.Plot(color=color, name=name, plotPoints=plotPoints) else: geom.Plot(ax, color, name, plotPoints=plotPoints) if plotLegend: ax.legend() # type: ignore [union-attr] return ax # type: ignore