Source code for opstool.vis.plotly.vis_eigen

from typing import Optional, Union

import numpy as np
import plotly.graph_objs as go
from plotly.subplots import make_subplots

from ...post import load_eigen_data, load_linear_buckling_data
from ...utils import CONFIGS, get_bounds
from .plot_resp_base import PlotResponsePlotlyBase
from .plot_utils import PLOT_ARGS, _plot_lines_cmap, _plot_unstru_cmap
from .vis_model import PlotModelBase

PKG_NAME = CONFIGS.get_pkg_name()
SHAPE_MAP = CONFIGS.get_shape_map()


class PlotEigenBase(PlotResponsePlotlyBase):
    def __init__(self, model_info, modal_props, eigen_vectors, interp_eigenvectors=None):
        self.nodal_data = model_info.get("NodalData", [])
        if len(self.nodal_data) > 0:
            self.nodal_tags = self.nodal_data.coords["nodeTags"]
            self.points = self.nodal_data.to_numpy()
            self.ndims = self.nodal_data.attrs["ndims"]
            self.bounds, self.min_bound_size, self.max_bound_size = get_bounds(self.points)
            self.show_zaxis = not np.max(self.ndims) <= 2
        else:
            raise ValueError("Model have no nodal data!")  # noqa: TRY003
        # -------------------------------------------------------------
        self.line_data = model_info.get("AllLineElesData", [])
        self.line_cells, self.line_tags = self._get_line_cells(self.line_data)
        # -------------------------------------------------------------
        self.unstru_data = model_info.get("UnstructuralData", [])
        self.unstru_tags, self.unstru_cell_types, self.unstru_cells = self._get_unstru_cells(self.unstru_data)
        # --------------------------------------------------
        self.ModelInfo = model_info
        self.ModalProps = modal_props
        self.EigenVectors = eigen_vectors
        self.InterpEigenVectors = interp_eigenvectors
        self.plot_model_base = PlotModelBase(model_info, {})

        if self.InterpEigenVectors is None:
            self.interplated_line_cells = []
            self.interplated_line_points = []
            self.interplated_eigen_vecs = []
        else:
            self.line_cells, self.line_tags = [], []
            self.interplated_line_points = self.InterpEigenVectors["points"].to_numpy()
            self.interplated_line_cells = self.InterpEigenVectors["cells"].to_numpy().astype(int)
            self.interplated_eigen_vecs = self.InterpEigenVectors["eigenVectors"].to_numpy()

        # plotly
        self.pargs = PLOT_ARGS
        self.FIGURE = go.Figure()

        self.title = {
            "font": {"family": "courier", "size": self.pargs.title_font_size},
            "text": f"<b>{PKG_NAME} :: Eigen 3D Viewer</b><br><br>",
        }

    def _get_eigen_points(self, step, alpha):
        eigen_vec = self.EigenVectors.to_numpy()[..., :3][step]
        value_ = np.max(np.sqrt(np.sum(eigen_vec**2, axis=1)))
        alpha_ = self.max_bound_size * self.pargs.scale_factor / value_
        alpha_ = alpha_ * alpha if alpha else alpha_
        eigen_points = self.points + eigen_vec * alpha_
        scalars = np.sqrt(np.sum(eigen_vec**2, axis=1))
        return eigen_points, scalars, alpha_

    def _get_eigen_points_interp(self, step, alpha_):
        eigen_vec = self.interplated_eigen_vecs[..., :3][step]
        eigen_points = self.interplated_line_points + eigen_vec * alpha_
        scalars = np.sqrt(np.sum(eigen_vec**2, axis=1))
        return eigen_points, scalars

    def _get_bc_points(self, step, scale: float):
        fixed_node_data = self.ModelInfo["FixedNodalData"]
        if len(fixed_node_data) > 0:
            fix_tags = fixed_node_data["nodeTags"].values
            coords = self.nodal_data.sel({"nodeTags": fix_tags}).to_numpy()
            eigen_vec = self.EigenVectors.sel({"nodeTags": fix_tags}).to_numpy()
            vec = eigen_vec[..., :3][step]
            coords = coords + vec * scale
        else:
            coords = []
        return coords

    def _make_eigen_txt(self, step):
        fi = self.ModalProps.loc[:, "eigenFrequency"][step]
        txt = f'<span style="font-weight:bold; font-size:{self.pargs.title_font_size}px">Mode {step + 1}</span>'
        # txt = f"<b>Mode {step + 1}</b>"
        period_txt = self._set_txt_props(f"{1 / fi:.6f}; ", color="blue")
        txt += f"<br><b>Period (s):</b> {period_txt}"
        fi_txt = self._set_txt_props(f"{fi:.6f};", color="blue")
        txt += f"<b>Frequency (Hz):</b> {fi_txt}"
        if not self.show_zaxis:
            txt += "<br><b>Modal participation mass ratios (%)</b><br>"
            mx = self.ModalProps.loc[:, "partiMassRatiosMX"][step]
            my = self.ModalProps.loc[:, "partiMassRatiosMY"][step]
            rmz = self.ModalProps.loc[:, "partiMassRatiosRMZ"][step]
            txt += self._set_txt_props(f"{mx:7.3f} {my:7.3f} {rmz:7.3f}", color="blue")
            txt += "<br><b>Cumulative modal participation mass ratios (%)</b><br>"
            mx = self.ModalProps.loc[:, "partiMassRatiosCumuMX"][step]
            my = self.ModalProps.loc[:, "partiMassRatiosCumuMY"][step]
            rmz = self.ModalProps.loc[:, "partiMassRatiosCumuRMZ"][step]
            txt += self._set_txt_props(f"{mx:7.3f} {my:7.3f} {rmz:7.3f}", color="blue")
            txt += "<br><b>{:>7} {:>7} {:>7}</b>".format("X", "Y", "RZ")
        else:
            txt += "<br><b>Modal participation mass ratios (%)</b><br>"
            mx = self.ModalProps.loc[:, "partiMassRatiosMX"][step]
            my = self.ModalProps.loc[:, "partiMassRatiosMY"][step]
            mz = self.ModalProps.loc[:, "partiMassRatiosMZ"][step]
            rmx = self.ModalProps.loc[:, "partiMassRatiosRMX"][step]
            rmy = self.ModalProps.loc[:, "partiMassRatiosRMY"][step]
            rmz = self.ModalProps.loc[:, "partiMassRatiosRMZ"][step]
            txt += self._set_txt_props(f"{mx:7.3f} {my:7.3f} {mz:7.3f} {rmx:7.3f} {rmy:7.3f} {rmz:7.3f}", color="blue")
            txt += "<br><b>Cumulative modal participation mass ratios (%)</b><br>"
            mx = self.ModalProps.loc[:, "partiMassRatiosCumuMX"][step]
            my = self.ModalProps.loc[:, "partiMassRatiosCumuMY"][step]
            mz = self.ModalProps.loc[:, "partiMassRatiosCumuMZ"][step]
            rmx = self.ModalProps.loc[:, "partiMassRatiosCumuRMX"][step]
            rmy = self.ModalProps.loc[:, "partiMassRatiosCumuRMY"][step]
            rmz = self.ModalProps.loc[:, "partiMassRatiosCumuRMZ"][step]
            txt += self._set_txt_props(f"{mx:7.3f} {my:7.3f} {mz:7.3f} {rmx:7.3f} {rmy:7.3f} {rmz:7.3f}", color="blue")
            txt += f"<br><b>{'X':>7} {'Y':>7} {'Z':>7} {'RX':>7} {'RY':>7} {'RZ':>7}</b>"
            # f'<span style="color:blue; font-weight:bold;">{"X":>7} {"Y":>7} {"Z":>7} {"RX":>7} {"RY":>7} {"RZ":>7}</span>'
        return txt

    def _make_eigen_subplots_txt(self, step):
        f = self.ModalProps.loc[:, "eigenFrequency"]
        mode = self._set_txt_props(f"{step + 1}", color="#8eab12")
        period = 1 / f[step]
        t = self._set_txt_props(f"{period:.3E}") if period < 0.001 else self._set_txt_props(f"{period:.3f}")
        txt = f"Mode <b>{mode}</b>: T = <b>{t}</b> s"
        return txt

    def _create_mesh(
        self,
        plotter: list,
        idx,
        coloraxis,
        alpha=1.0,
        style="surface",
        show_origin=False,
        show_bc: bool = True,
        bc_scale: float = 1.0,
        show_mp_constraint: bool = True,
    ):
        step = round(idx) - 1
        eigen_points, scalars, alpha_ = self._get_eigen_points(step, alpha)

        if show_origin:
            self.plot_model_base.plot_model_one_color(
                plotter,
                color="gray",
                style="wireframe",
            )

        if len(self.unstru_data) > 0:
            (
                face_points,
                face_line_points,
                face_mid_points,
                veci,
                vecj,
                veck,
                face_scalars,
                face_line_scalars,
            ) = self._get_plotly_unstru_data(eigen_points, self.unstru_cell_types, self.unstru_cells, scalars)
            _plot_unstru_cmap(
                plotter,
                face_points,
                veci=veci,
                vecj=vecj,
                veck=veck,
                scalars=face_scalars,
                coloraxis=coloraxis,
                style=style,
                line_width=self.pargs.line_width,
                opacity=self.pargs.mesh_opacity,
                show_edges=self.pargs.show_mesh_edges,
                edge_color=self.pargs.mesh_edge_color,
                edge_width=self.pargs.mesh_edge_width,
                edge_points=face_line_points,
                edge_scalars=face_line_scalars,
            )
        if self.InterpEigenVectors is None and len(self.line_data) > 0:
            line_points, _, line_scalars = self._get_plotly_line_data(eigen_points, self.line_cells, scalars)
            _plot_lines_cmap(
                plotter,
                line_points,
                scalars=line_scalars,
                coloraxis=coloraxis,
                width=self.pargs.line_width,
            )
        elif self.InterpEigenVectors is not None and len(self.interplated_line_cells) > 0:
            eigen_points_interp, scalars_interp = self._get_eigen_points_interp(step, alpha_)
            line_points, _, line_scalars = self._get_plotly_line_data(
                eigen_points_interp, self.interplated_line_cells, scalars_interp
            )
            _plot_lines_cmap(
                plotter,
                line_points,
                scalars=line_scalars,
                coloraxis=coloraxis,
                width=self.pargs.line_width,
            )
        if show_bc:
            bc_points = self._get_bc_points(step, scale=alpha_)
            self.plot_model_base.plot_bc(plotter, bc_scale, points_new=bc_points)
        if show_mp_constraint:
            self.plot_model_base.plot_mp_constraint(
                plotter,
                points_new=eigen_points,
            )

    def subplots(self, modei, modej, show_outline, **kargs):
        if modej - modei + 1 > 64:
            raise ValueError("When subplots True, mode_tag range must < 64 for clarify")  # noqa: TRY003
        shape = SHAPE_MAP[modej - modei + 1]
        specs = [[{"is_3d": True} for _ in range(shape[1])] for _ in range(shape[0])]
        subplot_titles = []
        for i, idx in enumerate(range(modei, modej + 1)):  # noqa: B007
            txt = self._make_eigen_subplots_txt(idx - 1)
            subplot_titles.append(txt)
        self.FIGURE = make_subplots(
            rows=shape[0],
            cols=shape[1],
            specs=specs,
            figure=self.FIGURE,
            print_grid=False,
            subplot_titles=subplot_titles,
            horizontal_spacing=0.07 / shape[1],
            vertical_spacing=0.1 / shape[0],
            column_widths=[1] * shape[1],
            row_heights=[1] * shape[0],
        )
        for i, idx in enumerate(range(modei, modej + 1)):
            idxi = int(np.ceil((i + 1) / shape[1]) - 1)
            idxj = int(i - idxi * shape[1])
            plotter = []
            self._create_mesh(plotter, idx, coloraxis=f"coloraxis{i + 1}", **kargs)
            self.FIGURE.add_traces(plotter, rows=idxi + 1, cols=idxj + 1)
        if not self.show_zaxis:
            scene = self._get_plotly_dim_scene(mode="2d", show_outline=show_outline)
        else:
            scene = self._get_plotly_dim_scene(mode="3d", show_outline=show_outline)
        scenes = {}
        coloraxiss = {}
        for k in range(shape[0] * shape[1]):
            coloraxiss[f"coloraxis{k + 1}"] = {"showscale": False, "colorscale": self.pargs.cmap}
            if k >= 1:
                if not self.show_zaxis:
                    scenes[f"scene{k + 1}"] = self._get_plotly_dim_scene(mode="2d", show_outline=show_outline)
                else:
                    scenes[f"scene{k + 1}"] = self._get_plotly_dim_scene(mode="3d", show_outline=show_outline)
        self.FIGURE.update_layout(
            font={"family": self.pargs.font_family},
            template=self.pargs.theme,
            autosize=True,
            showlegend=False,
            coloraxis={"showscale": False, "colorscale": self.pargs.cmap},
            scene=scene,
            **scenes,
            **coloraxiss,
        )

        return self.FIGURE

    def plot_slides(self, modei, modej, **kargs):
        n_data = None
        for i, idx in enumerate(range(modei, modej + 1)):
            plotter = []
            self._create_mesh(plotter, idx, coloraxis=f"coloraxis{i + 1}", **kargs)
            self.FIGURE.add_traces(plotter)
            if i == 0:
                n_data = len(self.FIGURE.data)
        for i in range(n_data, len(self.FIGURE.data)):
            self.FIGURE.data[i].visible = False
        # Create and add slider
        steps = []
        for i, idx in enumerate(range(modei, modej + 1)):
            # txt = "Mode {}: T = {:.3f} s".format(idx, 1 / f[idx - 1])
            txt = self._make_eigen_txt(idx - 1)
            txt = {"font": {"family": "courier", "size": self.pargs.font_size}, "text": txt}
            step = {
                "method": "update",
                "args": [{"visible": [False] * len(self.FIGURE.data)}, {"title": txt}],  # layout attribute
                "label": str(idx),
            }
            step["args"][0]["visible"][n_data * i : n_data * (i + 1)] = [True] * n_data
            # Toggle i'th trace to "visible"
            steps.append(step)
        sliders = [
            {
                "active": 0,
                "currentvalue": {"prefix": "Mode: "},
                "pad": {"t": 50},
                "steps": steps,
            }
        ]
        coloraxiss = {}
        for i in range(modej - modei + 1):
            coloraxiss[f"coloraxis{i + 1}"] = {
                "colorscale": self.pargs.cmap,
                # cmin=cmins[i],
                # cmax=cmaxs[i],
                "showscale": False,
                "colorbar": {"tickfont": {"size": self.pargs.font_size - 2}},
            }
        self.FIGURE.update_layout(sliders=sliders, **coloraxiss)

        return self.FIGURE

    def plot_anim(
        self,
        mode_tag: int = 1,
        n_cycle: int = 5,
        framerate: int = 1,
        alpha: float = 1.0,
        **kargs,
    ):
        alphas = [0.0] + [alpha, -alpha] * n_cycle
        duration = 1000 / framerate  # convert to milliseconds
        # ---------------------frames--------------------------------------------------------
        # start plot
        frames = []
        for k, alpha in enumerate(alphas):
            plotter = []
            self._create_mesh(plotter, mode_tag, alpha=alpha, coloraxis="coloraxis", **kargs)
            frames.append(go.Frame(data=plotter, name="step:" + str(k + 1)))
        # Add data to be displayed before animation starts
        plotter0 = []
        self._create_mesh(plotter0, mode_tag, alpha=alpha, coloraxis="coloraxis", **kargs)

        self.FIGURE = go.Figure(data=plotter0, frames=frames)

        # Layout
        txt = self._make_eigen_txt(mode_tag - 1)
        self.title["text"] += txt
        self.FIGURE.update_layout(
            coloraxis={"colorscale": self.pargs.cmap, "showscale": False},
        )
        self._update_antimate_layout(duration=duration, is_response_step=False)

    def plot_props_table(self, modei, modej):
        df = self.ModalProps.to_pandas()[modei - 1 : modej]
        df = df.T
        fig = go.Figure(
            data=[
                go.Table(
                    header={"values": ["modeTags", *list(df.columns)]},
                    cells={
                        "values": [df.index] + [df[col].tolist() for col in df.columns],
                        "format": [""] + [".3E"] * len(df.columns),
                    },
                )
            ]
        )
        return fig


class PlotBucklingBase(PlotEigenBase):
    def __init__(self, model_info, eigen_values, eigen_vectors, interp_eigenvectors=None):
        super().__init__(model_info, eigen_values, eigen_vectors, interp_eigenvectors=interp_eigenvectors)

    def _make_eigen_txt(self, step):
        mode = self._set_txt_props(f"{step + 1}", color="#8eab12")
        fi = self.ModalProps.isel(modeTags=step)
        fi = self._set_txt_props(f"{fi:.3E}") if fi < 0.001 else self._set_txt_props(f"{fi:.3f}")
        txt = f"Mode <b>{mode}</b>:<br>k = <b>{fi}</b>"
        return txt

    def _make_eigen_subplots_txt(self, step):
        return self._make_eigen_txt(step)


[docs] def plot_eigen( mode_tags: Union[list, tuple, int], odb_tag: Optional[Union[int, str]] = None, subplots: bool = False, scale: float = 1.0, show_outline: bool = False, show_origin: bool = False, style: str = "surface", show_bc: bool = True, bc_scale: float = 1.0, show_mp_constraint: bool = False, solver: str = "-genBandArpack", mode: str = "eigen", interpolate_beam: bool = True, ) -> go.Figure: """Modal visualization. Parameters ---------- mode_tags: Union[List, Tuple] The modal range to visualize, [mode i, mode j]. odb_tag: Union[int, str], default: None Tag of output databases (ODB) to be visualized. If None, the data will be saved automatically. subplots: bool, default: False If True, multiple subplots are used to present mode i to mode j. Otherwise, they are presented as slides. scale: float, default: 1.0 Zoom the presentation size of the mode shapes. show_outline: bool, default: False Whether to display the outline of the model. show_origin: bool, default: False Whether to show the undeformed shape. style: str, default: surface Visualization mesh style of surfaces and solids. One of the following: style='surface' or style='wireframe' Defaults to 'surface'. Note that 'wireframe' only shows a wireframe of the outer geometry. show_bc: bool, default: True Whether to display boundary supports. bc_scale: float, default: 1.0 Scale the size of boundary support display. show_mp_constraint: bool, default: False Whether to show multipoint (MP) constraint. solver : str, optional, OpenSees' eigenvalue analysis solver, by default "-genBandArpack". mode: str, default: eigen The type of modal analysis, can be "eigen" or "buckling". If "eigen", it will plot the eigenvalues and eigenvectors. If "buckling", it will plot the buckling factors and modes. Added in v0.1.15. interpolate_beam: bool, default: True Whether to interpolate beam elements for better visualization. Only applicable for eigenvalue analysis. Returns ------- fig: `plotly.graph_objects.Figure <https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html>`_ You can use `fig.show()` to display, You can also use `fig.write_html("path/to/file.html")` to save as an HTML file, see `Interactive HTML Export in Python <https://plotly.com/python/interactive-html-export/>`_ """ if isinstance(mode_tags, int): mode_tags = [1, mode_tags] modei, modej = int(mode_tags[0]), int(mode_tags[1]) if mode.lower() == "eigen": resave = odb_tag is None odb_tag = "Auto" if odb_tag is None else odb_tag modalProps, eigenvectors, interp_eigenvectors, MODEL_INFO = load_eigen_data( odb_tag=odb_tag, mode_tag=mode_tags[-1], solver=solver, resave=resave, interpolate_beam=interpolate_beam ) if not interpolate_beam: interp_eigenvectors = None plotbase = PlotEigenBase(MODEL_INFO, modalProps, eigenvectors, interp_eigenvectors=interp_eigenvectors) elif mode.lower() == "buckling": modalProps, eigenvectors, MODEL_INFO = load_linear_buckling_data(odb_tag=odb_tag) plotbase = PlotBucklingBase(MODEL_INFO, modalProps, eigenvectors) else: raise ValueError(f"Unsupported mode: {mode}. Use 'eigen' or 'buckling'.") # noqa: TRY003 if subplots: plotbase.subplots( modei, modej, show_outline=show_outline, # link_views=link_views, alpha=scale, style=style, show_origin=show_origin, show_bc=show_bc, bc_scale=bc_scale, show_mp_constraint=show_mp_constraint, ) else: plotbase.plot_slides( modei, modej, alpha=scale, style=style, show_origin=show_origin, show_bc=show_bc, bc_scale=bc_scale, show_mp_constraint=show_mp_constraint, ) return plotbase.update_fig(show_outline=show_outline)
[docs] def plot_eigen_animation( mode_tag: int, odb_tag: Optional[Union[int, str]] = None, n_cycle: int = 5, framerate: int = 3, scale: float = 1.0, solver: str = "-genBandArpack", show_outline: bool = False, show_origin: bool = False, style: str = "surface", show_bc: bool = True, bc_scale: float = 1.0, show_mp_constraint: bool = False, mode: str = "eigen", interpolate_beam: bool = True, ) -> go.Figure: """Modal animation visualization. Parameters ---------- mode_tag: int The mode tag to display. odb_tag: Union[int, str], default: None Tag of output databases (ODB) to be visualized. If None, the data will be saved automatically. n_cycle: int, default: five Number of cycles for the display. framerate: int, default: three Framerate for the display, i.e., the number of frames per second. scale: float, default: 1.0 Zoom the presentation size of the mode shapes. solver : str, optional, OpenSees' eigenvalue analysis solver, by default "-genBandArpack". show_outline: bool, default: False Whether to display the outline of the model. show_origin: bool, default: False Whether to show the undeformed shape. style: str, default: surface Visualization mesh style of surfaces and solids. One of the following: style='surface' or style='wireframe' Defaults to 'surface'. Note that 'wireframe' only shows a wireframe of the outer geometry. show_bc: bool, default: True Whether to display boundary supports. bc_scale: float, default: 1.0 Scale the size of boundary support display. show_mp_constraint: bool, default: False Whether to show multipoint (MP) constraint. mode: str, default: eigen The type of modal analysis, can be "eigen" or "buckling". If "eigen", it will plot the eigenvalues and eigenvectors. If "buckling", it will plot the buckling factors and modes. Added in v0.1.15. interpolate_beam: bool, default: True Whether to interpolate beam elements for better visualization. Only applicable for eigenvalue analysis. Returns ------- fig: `plotly.graph_objects.Figure <https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html>`_ You can use `fig.show()` to display, You can also use `fig.write_html("path/to/file.html")` to save as an HTML file, see `Interactive HTML Export in Python <https://plotly.com/python/interactive-html-export/>`_ """ if mode.lower() == "eigen": resave = odb_tag is None modalProps, eigenvectors, interp_eigenvectors, MODEL_INFO = load_eigen_data( odb_tag=odb_tag, mode_tag=mode_tag, solver=solver, resave=resave, interpolate_beam=interpolate_beam ) if not interpolate_beam: interp_eigenvectors = None plotbase = PlotEigenBase(MODEL_INFO, modalProps, eigenvectors, interp_eigenvectors=interp_eigenvectors) elif mode.lower() == "buckling": modalProps, eigenvectors, MODEL_INFO = load_linear_buckling_data(odb_tag=odb_tag) plotbase = PlotBucklingBase(MODEL_INFO, modalProps, eigenvectors) else: raise ValueError(f"Unsupported mode: {mode}. Use 'eigen' or 'buckling'.") # noqa: TRY003 plotbase.plot_anim( mode_tag, n_cycle=n_cycle, framerate=framerate, alpha=scale, style=style, show_origin=show_origin, show_bc=show_bc, bc_scale=bc_scale, show_mp_constraint=show_mp_constraint, ) return plotbase.update_fig(show_outline=show_outline)
[docs] def plot_eigen_table( mode_tags: Union[list, tuple, int], odb_tag: Union[int, str] = 1, solver: str = "-genBandArpack", ) -> go.Figure: """Plot Modal Properties Table. Parameters ---------- mode_tags: Union[List, Tuple] The modal range to visualize, [mode i, mode j]. odb_tag: Union[int, str], default: None Tag of output databases (ODB) to be visualized. solver : str, optional, OpenSees' eigenvalue analysis solver, by default "-genBandArpack". Returns ------- fig: `plotly.graph_objects.Figure <https://plotly.com/python-api-reference/generated/plotly.graph_objects.Figure.html>`_ You can use `fig.show()` to display, You can also use `fig.write_html("path/to/file.html")` to save as an HTML file, see `Interactive HTML Export in Python <https://plotly.com/python/interactive-html-export/>`_ """ resave = odb_tag is None if isinstance(mode_tags, int): mode_tags = [1, mode_tags] modalProps, eigenvectors, interp_eigenvectors, MODEL_INFO = load_eigen_data( odb_tag=odb_tag, mode_tag=mode_tags[-1], solver=solver, resave=resave ) modei, modej = int(mode_tags[0]), int(mode_tags[1]) plotbase = PlotEigenBase(MODEL_INFO, modalProps, eigenvectors, interp_eigenvectors=interp_eigenvectors) fig = plotbase.plot_props_table(modei, modej) return fig