Source code for pyoma2.support.geometry.mixin

from __future__ import annotations

import typing
import warnings
from typing import Optional, Union

import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
import pandas as pd

from pyoma2.algorithms.data.result import BaseResult
from pyoma2.functions.gen import (
    check_on_geo1,
    check_on_geo2,
    read_excel_file,
)

from .data import Geometry1, Geometry2
from .mpl_plotter import Geo1MplPlotter, Geo2MplPlotter
from .pyvista_plotter import PvGeoPlotter

if typing.TYPE_CHECKING:
    try:
        import pyvista as pv
    except ImportError:
        warnings.warn(
            "Optional package 'pyvista' is not installed. Some features may not be available.",
            ImportWarning,
            stacklevel=2,
        )
        warnings.warn(
            "Install 'pyvista' with 'pip install pyvista' or 'pip install pyoma_2[pyvista]'",
            ImportWarning,
            stacklevel=2,
        )


[docs]class GeometryMixin: """ Mixin that gives the ability to define the geometry the instance of the setup class. This mixin provides methods to define the geometry setups for the instance, including sensor names, coordinates, directions, and optional elements like lines, surfaces, and background nodes. It also includes methods to plot the geometry setups using Matplotlib and PyVista for 2D and 3D visualization, respectively. """ geo1: typing.Optional[Geometry1] = None geo2: typing.Optional[Geometry2] = None
[docs] def def_geo1( self, # # MANDATORY sens_names: typing.Union[ typing.List[str], typing.List[typing.List[str]], pd.DataFrame, npt.NDArray[np.str_], ], # sensors' names sens_coord: pd.DataFrame, # sensors' coordinates sens_dir: npt.NDArray[np.int64], # sensors' directions # # OPTIONAL sens_lines: npt.NDArray[np.int64] = None, # lines connecting sensors bg_nodes: npt.NDArray[np.float64] = None, # Background nodes bg_lines: npt.NDArray[np.int64] = None, # Background lines bg_surf: npt.NDArray[np.float64] = None, # Background surfaces ) -> None: """ Defines the first geometry setup (geo1) for the instance. This method sets up the geometry involving sensors' names, coordinates, directions, and optional elements like sensor lines, background nodes, lines, and surfaces. Parameters ---------- sens_names : Union[numpy.ndarray of string, List of string] An array or list containing the names of the sensors. sens_coord : pandas.DataFrame A DataFrame containing the coordinates of the sensors. sens_dir : numpy.ndarray of int64 An array defining the directions of the sensors. sens_lines : numpy.ndarray of int64, optional An array defining lines connecting sensors. Default is None. bg_nodes : numpy.ndarray of float64, optional An array defining background nodes. Default is None. bg_lines : numpy.ndarray of int64, optional An array defining background lines. Default is None. bg_surf : numpy.ndarray of float64, optional An array defining background surfaces. Default is None. """ # Get reference index (if any) ref_ind = getattr(self, "ref_ind", None) # Assemble dictionary for check function file_dict = { "sensors names": sens_names, "sensors coordinates": sens_coord, "sensors directions": sens_dir, "sensors lines": sens_lines if sens_lines is not None else pd.DataFrame(), "BG nodes": bg_nodes if bg_nodes is not None else pd.DataFrame(), "BG lines": bg_lines if bg_lines is not None else pd.DataFrame(), "BG surfaces": bg_surf if bg_surf is not None else pd.DataFrame(), } # check on input res_ok = check_on_geo1(file_dict, ref_ind=ref_ind) self.geo1 = Geometry1( sens_names=res_ok[0], sens_coord=res_ok[1], sens_dir=res_ok[2], sens_lines=res_ok[3], bg_nodes=res_ok[4], bg_lines=res_ok[5], bg_surf=res_ok[6], )
# metodo per definire geometria 2
[docs] def def_geo2( self, # MANDATORY sens_names: typing.Union[ typing.List[str], typing.List[typing.List[str]], pd.DataFrame, npt.NDArray[np.str_], ], # sensors' names pts_coord: pd.DataFrame, # points' coordinates sens_map: pd.DataFrame, # mapping # OPTIONAL cstr: pd.DataFrame = None, sens_sign: pd.DataFrame = None, sens_lines: npt.NDArray[np.int64] = None, # lines connecting sensors sens_surf: npt.NDArray[np.int64] = None, # surf connecting sensors bg_nodes: npt.NDArray[np.float64] = None, # Background nodes bg_lines: npt.NDArray[np.float64] = None, # Background lines bg_surf: npt.NDArray[np.float64] = None, # Background lines ) -> None: """ Defines the second geometry setup (geo2) for the instance. This method sets up an alternative geometry configuration, including sensors' names, points' coordinates, mapping, sign data, and optional elements like constraints, sensor lines, background nodes, lines, and surfaces. Parameters ---------- sens_names : Union[list of str, list of list of str, pandas.DataFrame, numpy.ndarray of str] Sensors' names. It can be a list of strings, a list of lists of strings, a DataFrame, or a NumPy array. pts_coord : pandas.DataFrame A DataFrame containing the coordinates of the points. sens_map : pandas.DataFrame A DataFrame containing the mapping data for sensors. cstrn : pandas.DataFrame, optional A DataFrame containing constraints. Default is None. sens_sign : pandas.DataFrame, optional A DataFrame containing sign data for the sensors. Default is None. sens_lines : numpy.ndarray of int64, optional An array defining lines connecting sensors. Default is None. bg_nodes : numpy.ndarray of float64, optional An array defining background nodes. Default is None. bg_lines : numpy.ndarray of float64, optional An array defining background lines. Default is None. bg_surf : numpy.ndarray of float64, optional An array defining background surfaces. Default is None. Notes ----- This method adapts indices for 0-indexed lines in `bg_lines`, `sens_lines`, and `bg_surf`. """ # Get reference index ref_ind = getattr(self, "ref_ind", None) # Assemble dictionary for check function file_dict = { "sensors names": sens_names, "points coordinates": pts_coord, "mapping": sens_map, "constraints": cstr if cstr is not None else pd.DataFrame(), "sensors sign": sens_sign if sens_sign is not None else pd.DataFrame(), "sensors lines": sens_lines if sens_lines is not None else pd.DataFrame(), "sensors surfaces": sens_surf if sens_surf is not None else pd.DataFrame(), "BG nodes": bg_nodes if bg_nodes is not None else pd.DataFrame(), "BG lines": bg_lines if bg_lines is not None else pd.DataFrame(), "BG surfaces": bg_surf if bg_surf is not None else pd.DataFrame(), } # check on input res_ok = check_on_geo2(file_dict, ref_ind=ref_ind) # Save to geometry self.geo2 = Geometry2( sens_names=res_ok[0], pts_coord=res_ok[1].astype(float), sens_map=res_ok[2], cstrn=res_ok[3], sens_sign=res_ok[4], sens_lines=res_ok[5], sens_surf=res_ok[6], bg_nodes=res_ok[7], bg_lines=res_ok[8], bg_surf=res_ok[9], )
def _def_geo_by_file( self, geo_type: str, path: str, **read_excel_file_kwargs ) -> None: """ Defines the geometry setup from an Excel file. This method reads an Excel file to extract sensor information, including sensor names, coordinates, and other optional geometry elements such as lines and background nodes. The information is used to set up the geometry for the instance. Parameters ---------- geo_type : str The type of geometry to define: 'geo1' or 'geo2'. path : str The file path to the Excel file containing the geometry data. read_excel_file_kwargs : dict, optional Additional keyword arguments to pass to the `read_excel_file` function. Raises ------ ValueError If the input data is invalid or missing required fields. """ # Get reference index ref_ind = getattr(self, "ref_ind", None) # Read the Excel file file_dict = read_excel_file(path=path, **read_excel_file_kwargs) # Check on input if geo_type == "geo1": res_ok = check_on_geo1(file_dict, ref_ind=ref_ind) self.geo1 = Geometry1( sens_names=res_ok[0], sens_coord=res_ok[1], sens_dir=res_ok[2], sens_lines=res_ok[3], bg_nodes=res_ok[4], bg_lines=res_ok[5], bg_surf=res_ok[6], ) elif geo_type == "geo2": res_ok = check_on_geo2(file_dict, ref_ind=ref_ind) self.geo2 = Geometry2( sens_names=res_ok[0], pts_coord=res_ok[1].astype(float), sens_map=res_ok[2], cstrn=res_ok[3], sens_sign=res_ok[4], sens_lines=res_ok[5], sens_surf=res_ok[6], bg_nodes=res_ok[7], bg_lines=res_ok[8], bg_surf=res_ok[9], ) else: raise ValueError(f"Invalid geometry type: {geo_type}") # metodo per definire geometria 1 da file
[docs] def def_geo1_by_file(self, path: str, **read_excel_file_kwargs) -> None: """ Defines the first geometry (geo1) from an Excel file. This method reads an Excel file to extract sensor information, including sensor names, coordinates, and other optional geometry elements such as lines and background nodes. The information is used to set up the geometry for the instance. Parameters ---------- path : str The file path to the Excel file containing the geometry data. read_excel_file_kwargs : dict, optional Additional keyword arguments to pass to the `read_excel_file` function. Raises ------ ValueError If the input data is invalid or missing required fields. """ self._def_geo_by_file(geo_type="geo1", path=path, **read_excel_file_kwargs)
[docs] def def_geo2_by_file(self, path: str, **read_excel_file_kwargs) -> None: """ Defines the second geometry (geo2) from an Excel file. This method reads an Excel file to extract information related to the geometry configuration, including sensor names, points' coordinates, mapping, and optional background nodes and surfaces. The information is used to set up the second geometry for the instance. Parameters ---------- path : str The file path to the Excel file containing the geometry data. read_excel_file_kwargs : dict, optional Additional keyword arguments to pass to the `read_excel_file` function. Raises ------ ValueError If the input data is invalid or missing required fields. """ self._def_geo_by_file(geo_type="geo2", path=path, **read_excel_file_kwargs)
# PLOT GEO1 - mpl plotter
[docs] def plot_geo1( self, scaleF: int = 1, view: typing.Literal["3D", "xy", "xz", "yz"] = "3D", col_sns: str = "red", col_sns_lines: str = "red", col_BG_nodes: str = "gray", col_BG_lines: str = "gray", col_BG_surf: str = "gray", col_txt: str = "red", ) -> typing.Tuple[plt.Figure, plt.Axes]: """ Plots the first geometry setup (geo1) using Matplotlib. This method creates a 2D or 3D plot of the first geometry, including sensors, lines, background nodes, and surfaces, using customizable color schemes for each element. Parameters ---------- scaleF : int, optional Scaling factor for the plot. Default is 1. view : {'3D', 'xy', 'xz', 'yz'}, optional The view angle of the plot. Default is '3D'. col_sns : str, optional Color of the sensors. Default is 'red'. col_sns_lines : str, optional Color of the lines connecting sensors. Default is 'red'. col_BG_nodes : str, optional Color of the background nodes. Default is 'gray'. col_BG_lines : str, optional Color of the background lines. Default is 'gray'. col_BG_surf : str, optional Color of the background surfaces. Default is 'gray'. col_txt : str, optional Color of the text labels for sensors. Default is 'red'. Returns ------- tuple A tuple containing the Matplotlib figure and axes objects. Raises ------ ValueError If `geo1` is not defined. """ if self.geo1 is None: raise ValueError("geo1 is not defined. Call def_geo1 first.") Plotter = Geo1MplPlotter(self.geo1) fig, ax = Plotter.plot_geo( scaleF, view, col_sns, col_sns_lines, col_BG_nodes, col_BG_lines, col_BG_surf, col_txt, ) return fig, ax
# PLOT GEO2 - PyVista plotter # PLOT GEO2 - PyVista plotter
[docs] def plot_geo2( self, *, scaleF: float = 1.0, col_sens: str = "red", show_points: bool = True, show_lines: bool = True, show_surf: bool = True, points_sett: Optional[dict] = None, lines_sett: Optional[dict] = None, surf_sett: Optional[dict] = None, background: bool = True, notebook: bool = False, ) -> pv.Plotter: """ Plots the second geometry setup (geo2) using PyVista for 3D visualization. This method creates a 3D interactive plot of the second geometry setup with options to visualize sensor points, connecting lines, and surfaces. It provides various customization options for coloring and rendering. Parameters ---------- scaleF : float, optional Scaling factor for sensor arrow length. Default is 1.0. col_sens : str, optional Color of the sensors. Default is 'red'. show_points : bool, optional Whether to plot sensor points. Default is True. show_lines : bool, optional Whether to plot lines connecting sensors. Default is True. show_surf : bool, optional Whether to plot surfaces connecting sensors. Default is True. points_sett : dict or None, optional Settings for the points' appearance; falls back to defaults if None. lines_sett : dict or None, optional Settings for the lines' appearance; falls back to defaults if None. surf_sett : dict or None, optional Settings for the surfaces' appearance; falls back to defaults if None. background : bool, optional Whether to use a background Qt plotter if creating new. Default is True. notebook : bool, optional Whether to render the plot in a Jupyter notebook environment. Default is False. Returns ------- pyvista.Plotter A PyVista Plotter object with the geometry visualization. Raises ------ ValueError If `geo2` is not defined. """ if self.geo2 is None: raise ValueError("geo2 is not defined. Call def_geo2 first.") Plotter = PvGeoPlotter(self.geo2) pl = Plotter.plot_geo( scaleF=scaleF, col_sens=col_sens, show_points=show_points, points_sett=points_sett, show_lines=show_lines, lines_sett=lines_sett, show_surf=show_surf, surf_sett=surf_sett, pl=None, background=background, notebook=notebook, ) return pl
# PLOT GEO2 - Matplotlib plotter
[docs] def plot_geo2_mpl( self, scaleF: int = 1, view: typing.Literal["3D", "xy", "xz", "yz", "x", "y", "z"] = "3D", col_sns: str = "red", col_sns_lines: str = "black", col_sns_surf: str = "lightcoral", col_BG_nodes: str = "gray", col_BG_lines: str = "gray", col_BG_surf: str = "gray", col_txt: str = "red", ) -> typing.Tuple[plt.Figure, plt.Axes]: """ Plots the second geometry setup (geo2) using Matplotlib. This method creates a 2D or 3D plot of the second geometry, including sensors, lines, surfaces, background nodes, and surfaces, with customizable colors. Parameters ---------- scaleF : int, optional Scaling factor for the plot. Default is 1. view : {'3D', 'xy', 'xz', 'yz', 'x', 'y', 'z'}, optional The view angle of the plot. Default is '3D'. col_sns : str, optional Color of the sensors. Default is 'red'. col_sns_lines : str, optional Color of the lines connecting sensors. Default is 'black'. col_sns_surf : str, optional Color of the surfaces connecting sensors. Default is 'lightcoral'. col_BG_nodes : str, optional Color of the background nodes. Default is 'gray'. col_BG_lines : str, optional Color of the background lines. Default is 'gray'. col_BG_surf : str, optional Color of the background surfaces. Default is 'gray'. col_txt : str, optional Color of the text labels for sensors. Default is 'red'. Returns ------- tuple A tuple containing the Matplotlib figure and axes objects. Raises ------ ValueError If `geo2` is not defined. """ if self.geo2 is None: raise ValueError("geo2 is not defined. Call def_geo2 first.") Plotter = Geo2MplPlotter(self.geo2) fig, ax = Plotter.plot_geo( scaleF, view, col_sns, col_sns_lines, col_sns_surf, col_BG_nodes, col_BG_lines, col_BG_surf, col_txt, ) return fig, ax
[docs] def plot_mode_geo1( self, algo_res: BaseResult, mode_nr: int, scaleF: int = 1, view: typing.Literal["3D", "xy", "xz", "yz"] = "3D", col_sns: str = "red", col_sns_lines: str = "red", col_BG_nodes: str = "gray", col_BG_lines: str = "gray", col_BG_surf: str = "gray", ) -> typing.Tuple[plt.Figure, plt.Axes]: """ Plots the mode shapes for the first geometry setup (geo1) using Matplotlib. This method visualizes the mode shapes corresponding to the specified mode number, with customizable colors and scaling for different geometrical elements such as sensors, lines, and background surfaces. Parameters ---------- algo_res : BaseResult The result object containing modal parameters and mode shape data. mode_nr : int The mode number to be plotted. scaleF : int, optional Scaling factor to adjust the size of the mode shapes. Default is 1. view : {'3D', 'xy', 'xz', 'yz'}, optional The viewing plane or angle for the plot. Default is '3D'. col_sns : str, optional Color of the sensors in the plot. Default is 'red'. col_sns_lines : str, optional Color of the lines connecting the sensors. Default is 'red'. col_BG_nodes : str, optional Color of the background nodes in the plot. Default is 'gray'. col_BG_lines : str, optional Color of the background lines in the plot. Default is 'gray'. col_BG_surf : str, optional Color of the background surfaces in the plot. Default is 'gray'. Returns ------- tuple A tuple containing the Matplotlib figure and axes objects for further customization or saving. Raises ------ ValueError If `geo1` is not defined or if the algorithm results are missing. """ if self.geo1 is None: raise ValueError("geo1 is not defined. Call def_geo1 first.") if algo_res.Fn is None: raise ValueError("Run algorithm first") Plotter = Geo1MplPlotter(self.geo1, algo_res) fig, ax = Plotter.plot_mode( mode_nr, scaleF, view, col_sns, col_sns_lines, col_BG_nodes, col_BG_lines, col_BG_surf, ) return fig, ax
# PLOT MODI - PyVista plotter
[docs] def plot_mode_geo2( self, algo_res: BaseResult, *, mode_nr: int = 1, scaleF: float = 1.0, show_lines: bool = True, show_surf: bool = True, def_sett: Optional[dict] = None, undef_sett: Optional[dict] = None, background: bool = True, notebook: bool = False, ) -> pv.Plotter: """ Plots the mode shapes for the second geometry setup (geo2) using PyVista. This method creates an interactive 3D plot of a single mode shape (with undeformed geometry underneath) for the second geometry setup. You can toggle connection lines and surface faces, and supply custom plot settings. Parameters ---------- algo_res : BaseResult The result object containing modal parameters and mode shape data. mode_nr : int, optional Mode number to visualize (1-based). Default is 1. scaleF : float, optional Scale factor for deformation amplitude. Default is 1.0. show_lines : bool, optional Whether to render connection lines on the mode shape. Default is True. show_surf : bool, optional Whether to render surface faces on the mode shape. Default is True. def_sett : dict or None, optional Plot settings for the deformed shape; falls back to defaults if None. undef_sett : dict or None, optional Plot settings for the undeformed shape; falls back to defaults if None. background : bool, optional Whether to use a background Qt plotter if creating new. Default is True. notebook : bool, optional Whether to render the plot in a Jupyter notebook environment. Default is False. Returns ------- pv.Plotter A PyVista Plotter object with the mode‐shape visualization. Raises ------ ValueError If `geo2` is not defined or if `algo_res.Fn` is None. """ if self.geo2 is None: raise ValueError("geo2 is not defined. Call def_geo2 first.") if algo_res.Fn is None: raise ValueError("Run algorithm first") Plotter = PvGeoPlotter(self.geo2, algo_res) pl = Plotter.plot_mode( mode_nr=mode_nr, scaleF=scaleF, show_lines=show_lines, show_surf=show_surf, def_sett=def_sett, undef_sett=undef_sett, pl=None, background=background, notebook=notebook, ) return pl
# PLOT MODI - Matplotlib plotter
[docs] def plot_mode_geo2_mpl( self, algo_res: BaseResult, mode_nr: typing.Optional[int], scaleF: int = 1, view: typing.Literal["3D", "xy", "xz", "yz"] = "3D", color: str = "cmap", *args, **kwargs, ) -> typing.Tuple[plt.Figure, plt.Axes]: """ Plots the mode shapes for the second geometry setup (geo2) using Matplotlib. This method visualizes the mode shapes for geo2, with customizable scaling, color, and viewing options. The plot can be configured for different modes and color maps. Parameters ---------- algo_res : BaseResult The result object containing modal parameters and mode shape data. mode_nr : int, optional The mode number to be plotted. If None, the default mode is plotted. scaleF : int, optional Scaling factor to adjust the size of the mode shapes. Default is 1. view : {'3D', 'xy', 'xz', 'yz'}, optional The viewing plane or angle for the plot. Default is '3D'. color : str, optional Color scheme or colormap to be used for the mode shapes. Default is 'cmap'. Returns ------- tuple A tuple containing the Matplotlib figure and axes objects for further customization or saving. Raises ------ ValueError If `geo2` is not defined or if the algorithm results are missing (e.g., `Fn` is None). """ if self.geo2 is None: raise ValueError("geo2 is not defined. Call def_geo2 first.") if algo_res.Fn is None: raise ValueError("Run algorithm first") Plotter = Geo2MplPlotter(self.geo2, algo_res) fig, ax = Plotter.plot_mode(mode_nr, scaleF, view, color) return fig, ax
# PLOT MODI - PyVista plotter
[docs] def anim_mode_geo2( self, algo_res: BaseResult, *, mode_nr: int = 1, scaleF: float = 1.0, show_lines: bool = True, show_surf: bool = True, def_sett: Optional[dict] = None, save_gif: bool = False, pl: Optional[pv.Plotter] = None, ) -> Union[pv.Plotter, str]: """ Creates an animation of the mode shape for the second geometry setup (geo2). This wraps PvGeoPlotter.animate_mode, letting you animate a single mode (with optional GIF export) on geo2. Parameters ---------- algo_res : BaseResult The result object containing modal parameters and mode shape data. mode_nr : int, optional Mode number to animate (1-based). Default is 1. scaleF : float, optional Scale factor for oscillation amplitude. Default is 1.0. show_lines : bool, optional Whether to render connection lines during the animation. Default is True. show_surf : bool, optional Whether to render surface faces during the animation. Default is True. def_sett : dict or None, optional Plot settings for animation frames; falls back to defaults if None. save_gif : bool, optional If True, save the animation as a GIF and return its filepath. Default is False. pl : pv.Plotter or None, optional Existing Plotter to use; if None, one is created. Default is None. Returns ------- pv.Plotter or str The Plotter instance for live animation, or filepath string if GIF was saved. Raises ------ ValueError If `geo2` is not defined or if `algo_res.Fn` is None. """ if self.geo2 is None: raise ValueError("geo2 is not defined. Call def_geo2 first.") if algo_res.Fn is None: raise ValueError("Run algorithm first") Plotter = PvGeoPlotter(self.geo2, algo_res) result = Plotter.animate_mode( mode_nr=mode_nr, scaleF=scaleF, show_lines=show_lines, show_surf=show_surf, def_sett=def_sett, save_gif=save_gif, pl=pl, ) return result