from functools import partial
from typing import Union
import numpy as np
import pyvista as pv
from .plot_resp_base import PlotResponseBase
from .plot_utils import (
PLOT_ARGS,
_plot_all_mesh,
_plot_lines,
_plot_lines_cmap,
_get_line_cells,
_get_unstru_cells,
)
from ...post import loadODB
from ...utils import gram_schmidt
class PlotTrussResponse(PlotResponseBase):
def __init__(self, model_info_steps, truss_resp_step, model_update):
super().__init__(model_info_steps, truss_resp_step, model_update)
def _get_truss_data(self, step):
return self._get_model_data("TrussData", step)
def _plot_all_mesh(self, plotter, color="gray", step=0):
pos = self._get_node_data(step).to_numpy()
line_cells, _ = _get_line_cells(self._get_line_data(step))
_, unstru_cell_types, unstru_cells = _get_unstru_cells(
self._get_unstru_data(step)
)
_plot_all_mesh(
plotter,
pos,
line_cells,
unstru_cells,
unstru_cell_types,
color=color,
render_lines_as_tubes=False,
)
def _set_resp_type(self, resp_type: str):
if resp_type.lower() in ["axialforce", "force"]:
resp_type = "axialForce"
elif resp_type.lower() in ["axialdefo", "axialdeformation", "deformation"]:
resp_type = "axialDefo"
elif resp_type.lower() in ["stress", "axialstress"]:
resp_type = "Stress"
elif resp_type.lower() in ["strain", "axialstrain"]:
resp_type = "Strain"
else:
raise ValueError(
f"Not supported response type {resp_type}! "
"Valid options are: axialForce, axialDefo, Stress, Strain."
)
self.resp_type = resp_type
def _make_truss_info(self, ele_tags, step):
pos = self._get_node_data(step).to_numpy()
truss_data = self._get_truss_data(step)
if ele_tags is None:
truss_tags = truss_data.coords["eleTags"].values
truss_cells = truss_data.to_numpy().astype(int)
else:
truss_tags = np.atleast_1d(ele_tags)
truss_cells = truss_data.sel(eleTags=truss_tags).to_numpy().astype(int)
truss_node_coords = []
truss_cells_new = []
for i, cell in enumerate(truss_cells):
nodei, nodej = cell[1:]
truss_node_coords.append(pos[nodei])
truss_node_coords.append(pos[nodej])
truss_cells_new.append([2, 2 * i, 2 * i + 1])
truss_node_coords = np.array(truss_node_coords)
return truss_tags, truss_node_coords, truss_cells_new
def refactor_resp_step(self, resp_type: str, ele_tags):
self._set_resp_type(resp_type)
resps = []
if self.ModelUpdate or ele_tags is not None:
for i in range(self.num_steps):
truss_tags, _, _ = self._make_truss_info(ele_tags, i)
da = self._get_resp_data(i, self.resp_type)
da = da.sel(eleTags=truss_tags)
resps.append(da)
else:
for i in range(self.num_steps):
da = self._get_resp_data(i, self.resp_type)
resps.append(da)
self.resp_step = resps # update
def _get_resp_peak(self):
resp_step = self.resp_step
maxv = [np.max(np.abs(data)) for data in resp_step]
maxstep = np.argmax(maxv)
maxv = np.max(maxv)
cmin, cmax = self._get_truss_resp_clim()
if maxv == 0:
alpha_ = 0.0
else:
alpha_ = (
self.max_bound_size * self.pargs.scale_factor / maxv
)
return maxstep, (cmin, cmax), alpha_
def _get_truss_resp_clim(self):
maxv = [np.max(data) for data in self.resp_step]
minv = [np.min(data) for data in self.resp_step]
cmin, cmax = np.min(minv), np.max(maxv)
return cmin, cmax
def _create_mesh(
self,
plotter,
value,
ele_tags=None,
show_values=True,
plot_all_mesh=True,
clim=None,
alpha=1.0,
line_width=1.5,
):
step = int(round(value))
truss_tags, truss_coords, truss_cells = self._make_truss_info(ele_tags, step)
resps = self.resp_step[step].to_numpy()
resp_points, resp_cells = [], []
scalars = []
label_points, labels = [], []
for cell, resp in zip(truss_cells, resps):
coord1 = np.array(truss_coords[cell[1]])
coord2 = np.array(truss_coords[cell[2]])
xaxis = coord2 - coord1
length = np.linalg.norm(xaxis)
xaxis = xaxis / length
cos_theta = np.dot(xaxis, [0, 0, 1])
if 1 - cos_theta**2 < 1e-4:
axis_up = [1, 0, 0]
elif self.show_zaxis:
axis_up = [0, 0, 1]
else:
axis_up = [0, 1, 0]
_, plot_axis, _ = gram_schmidt(xaxis, axis_up)
coord3 = coord1 + alpha * resp * plot_axis
coord4 = coord2 + alpha * resp * plot_axis
coord_upper = [coord3 + length / 12 * i * xaxis for i in range(13)] + [
coord4
]
coord_lower = [coord1 + length / 12 * i * xaxis for i in range(13)] + [
coord3
]
for i in range(len(coord_upper)):
resp_cells.append([2, len(resp_points), len(resp_points) + 1])
resp_points.extend([coord_lower[i], coord_upper[i]])
scalars.extend([resp, resp])
label_points.append((coord3 + coord4) / 2)
labels.append(resp)
labels = [f"{label:.3E}" for label in labels]
label_points = np.array(label_points)
resp_points = np.array(resp_points)
scalars = np.array(scalars)
# ---------------------------------
plotter.clear_actors() # !!!!!!
if plot_all_mesh:
self._plot_all_mesh(plotter, color="gray", step=step)
_ = _plot_lines(
plotter,
pos=truss_coords,
cells=truss_cells,
width=self.pargs.line_width,
color=self.pargs.color_truss,
render_lines_as_tubes=self.pargs.render_lines_as_tubes,
)
resp_plot = _plot_lines_cmap(
plotter,
resp_points,
resp_cells,
scalars,
width=line_width,
cmap=self.pargs.cmap,
clim=clim,
render_lines_as_tubes=self.pargs.render_lines_as_tubes,
show_scalar_bar=False,
)
t_ = self.time[step]
title = self.resp_type.capitalize() + "\n"
title += f"step: {step};" + f" time: {t_:.4f}\n"
title += "min = {:.3E}\nmax = {:.3E}\n".format(np.min(scalars), np.max(scalars))
_ = plotter.add_text(
title,
position="upper_right",
font_size=self.pargs.title_font_size,
font="courier",
)
_ = plotter.add_scalar_bar(
fmt="%.3e",
n_labels=10,
bold=True,
vertical=True,
font_family="courier",
label_font_size=self.pargs.font_size,
title_font_size=self.pargs.title_font_size,
position_x=0.875,
)
if show_values:
plotter.add_point_labels(
label_points,
labels,
# text_color="white",
font_size=self.pargs.font_size,
bold=True,
always_visible=True,
shape=None,
)
return resp_plot
def plot_slide(
self,
plotter,
ele_tags=None,
show_values=True,
alpha=1.0,
line_width=1.5,
):
plot_all_mesh = True if ele_tags is None else False
_, clim, alpha_ = self._get_resp_peak()
func = partial(
self._create_mesh,
plotter,
ele_tags=ele_tags,
clim=clim,
plot_all_mesh=plot_all_mesh,
show_values=show_values,
alpha=alpha * alpha_,
line_width=line_width,
)
plotter.add_slider_widget(
func,
[0, self.num_steps - 1],
value=self.num_steps - 1,
pointa=(0.01, 0.925),
pointb=(0.45, 0.925),
title="Step",
title_opacity=1,
# title_color="black",
fmt="%.0f",
title_height=0.03,
slider_width=0.03,
tube_width=0.008,
)
def plot_peak_step(
self,
plotter,
ele_tags=None,
show_values=True,
alpha=1.0,
line_width=1.5,
):
plot_all_mesh = True if ele_tags is None else False
max_step, clim, alpha_ = self._get_resp_peak()
self._create_mesh(
plotter=plotter,
value=max_step,
ele_tags=ele_tags,
show_values=show_values,
clim=clim,
plot_all_mesh=plot_all_mesh,
alpha=alpha * alpha_,
line_width=line_width,
)
def plot_anim(
self,
plotter,
ele_tags=None,
show_values=True,
alpha=1.0,
framerate: int = None,
savefig: str = "TrussRespAnimation.gif",
line_width=1.5,
):
if framerate is None:
framerate = np.ceil(self.num_steps / 10)
if savefig.endswith(".gif"):
plotter.open_gif(savefig, fps=framerate)
else:
plotter.open_movie(savefig, framerate=framerate)
plot_all_mesh = True if ele_tags is None else False
_, clim, alpha_ = self._get_resp_peak()
# plotter.write_frame() # write initial data
for step in range(self.num_steps):
self._create_mesh(
plotter,
step,
ele_tags=ele_tags,
show_values=show_values,
clim=clim,
plot_all_mesh=plot_all_mesh,
alpha=alpha * alpha_,
line_width=line_width,
)
plotter.write_frame()
def update(self, plotter, cpos):
viewer = {
"xy": plotter.view_xy,
"yx": plotter.view_yx,
"xz": plotter.view_xz,
"zx": plotter.view_zx,
"yz": plotter.view_yz,
"zy": plotter.view_zy,
"iso": plotter.view_isometric,
}
if not self.show_zaxis:
cpos = "xy"
viewer[cpos]()
return plotter
[docs]
def plot_truss_responses(
odb_tag: Union[int, str] = 1,
ele_tags: Union[int, list] = None,
slides: bool = False,
show_values: bool = True,
resp_type: str = "axialForce",
alpha: float = 1.0,
line_width: float = 1.5,
cpos: str = "iso",
):
"""Visualizing Truss Response.
Parameters
----------
odb_tag: Union[int, str], default: 1
Tag of output databases (ODB) to be visualized.
ele_tags: Union[int, list], default: None
The tags of truss elements to be visualized. If None, all truss elements are selected.
slides: bool, default: False
Display the response for each step in the form of a slideshow.
Otherwise, show the step with the largest response.
show_values: bool, default: True
Whether to display the response value.
resp_type: str, default: "axialForce"
Response type, optional, one of ["axialForce", "axialDefo", "Stress", "Strain"].
alpha: float, default: 1.0
Scale the size of the response graph.
line_width: float, default: 1.5.
Line width of the response graph.
cpos: str, default: iso
Model display perspective, optional: "iso", "xy", "yx", "xz", "zx", "yz", "zy".
If 3d, defaults to "iso". If 2d, defaults to "xy".
Returns
-------
Plotting object of PyVista to display vtk meshes or numpy arrays.
See `pyvista.Plotter <https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter>`_.
You can use
`Plotter.show <https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter.show#pyvista.Plotter.show>`_.
to display the plotting window.
You can also use
`Plotter.export_html <https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter.export_html#pyvista.Plotter.export_html>`_.
to export this plotter as an interactive scene to an HTML file.
"""
model_info_steps, model_update, truss_resp_step = loadODB(
odb_tag, resp_type="Truss"
)
plotter = pv.Plotter(
notebook=PLOT_ARGS.notebook,
line_smoothing=PLOT_ARGS.line_smoothing,
off_screen=PLOT_ARGS.off_screen
)
plotbase = PlotTrussResponse(model_info_steps, truss_resp_step, model_update)
plotbase.refactor_resp_step(resp_type=resp_type, ele_tags=ele_tags)
if slides:
plotbase.plot_slide(
plotter,
ele_tags=ele_tags,
show_values=show_values,
alpha=alpha,
line_width=line_width,
)
else:
plotbase.plot_peak_step(
plotter,
ele_tags=ele_tags,
show_values=show_values,
alpha=alpha,
line_width=line_width,
)
if PLOT_ARGS.anti_aliasing:
plotter.enable_anti_aliasing(PLOT_ARGS.anti_aliasing)
return plotbase.update(plotter, cpos)
[docs]
def plot_truss_responses_animation(
odb_tag: Union[int, str] = 1,
ele_tags: Union[int, list] = None,
framerate: int = None,
savefig: str = "TrussRespAnimation.gif",
show_values: bool = False,
resp_type: str = "axialForce",
alpha: float = 1.0,
line_width: float = 1.5,
cpos: str = "iso",
):
"""Truss response animation.
Parameters
----------
odb_tag: Union[int, str], default: 1
Tag of output databases (ODB) to be visualized.
ele_tags: Union[int, list], default: None
The tags of truss elements to be visualized. If None, all truss elements are selected.
framerate: int, default: None
Framerate for the display, i.e., the number of frames per second.
savefig: str, default: TrussRespAnimation.gif
Path to save the animation. The suffix can be ``.gif`` or ``.mp4``.
show_values: bool, default: False
Whether to display the response value.
resp_type: str, default: "axialForce"
Response type, optional, one of ["axialForce", "axialDefo", "Stress", "Strain"].
alpha: float, default: 1.0
Scale the size of the response graph.
line_width: float, default: 1.5.
Line width of the response graph.
cpos: str, default: iso
Model display perspective, optional: "iso", "xy", "yx", "xz", "zx", "yz", "zy".
If 3d, defaults to "iso". If 2d, defaults to "xy".
Returns
-------
Plotting object of PyVista to display vtk meshes or numpy arrays.
See `pyvista.Plotter <https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter>`_.
You can use
`Plotter.show <https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter.show#pyvista.Plotter.show>`_.
to display the plotting window.
You can also use
`Plotter.export_html <https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter.export_html#pyvista.Plotter.export_html>`_.
to export this plotter as an interactive scene to an HTML file.
"""
model_info_steps, model_update, truss_resp_step = loadODB(
odb_tag, resp_type="Truss"
)
plotter = pv.Plotter(
notebook=PLOT_ARGS.notebook,
line_smoothing=PLOT_ARGS.line_smoothing,
off_screen=PLOT_ARGS.off_screen
)
plotbase = PlotTrussResponse(model_info_steps, truss_resp_step, model_update)
plotbase.refactor_resp_step(resp_type=resp_type, ele_tags=ele_tags)
plotbase.plot_anim(
plotter,
ele_tags=ele_tags,
show_values=show_values,
alpha=alpha,
framerate=framerate,
savefig=savefig,
line_width=line_width,
)
if PLOT_ARGS.anti_aliasing:
plotter.enable_anti_aliasing(PLOT_ARGS.anti_aliasing)
return plotbase.update(plotter, cpos)