11"""Supports reading cyclic structural result files from ANSYS"""
22
3+ from collections .abc import Iterable
34from functools import wraps
45import warnings
56
67import numpy as np
78import pyvista as pv
9+ from tqdm import tqdm
810from vtkmodules .vtkCommonMath import vtkMatrix4x4
911from vtkmodules .vtkCommonTransforms import vtkTransform
1012from vtkmodules .vtkFiltersCore import vtkAppendFilter
1113
1214from ansys .mapdl .reader import _binary_reader
15+ from ansys .mapdl .reader ._rst_keys import element_index_table_info
1316from ansys .mapdl .reader .common import (
1417 PRINCIPAL_STRESS_TYPES ,
1518 STRAIN_TYPES ,
1619 STRESS_TYPES ,
1720 THERMAL_STRAIN_TYPES ,
1821 axis_rotation ,
1922)
20- from ansys .mapdl .reader .rst import Result , check_comp
23+ from ansys .mapdl .reader .rst import ELEMENT_INDEX_TABLE_KEYS , Result , check_comp
2124
2225np .seterr (divide = "ignore" , invalid = "ignore" )
2326
27+ # MAPDL results that are tensors
28+ RESULT_TENSORS_TYPES = [
29+ "ENS" , # nodal stresses
30+ "EEL" , # elastic strains
31+ "EPL" , # plastic strains
32+ "ECR" , # creep strains
33+ "ETH" , # thermal strains
34+ "EDI" , # diffusion strains
35+ "EBA" , # back stresses
36+ ]
37+
38+ # MAPDL results that are stresses
39+ RESULT_STRESS_TYPES = [
40+ "ENS" , # nodal stresses
41+ "ESF" , # element surface stresses
42+ "EBA" , # back stresses
43+ ]
44+
2445
2546class CyclicResult (Result ):
2647 """Adds cyclic functionality to the result class"""
@@ -1595,9 +1616,12 @@ def animate_nodal_solution(
15951616 movie_filename = None ,
15961617 ** kwargs ,
15971618 ):
1598- """Animate nodal solution. Assumes nodal solution is a
1599- displacement array from a modal solution.
1619+ """Animate nodal solution.
1620+
1621+ Assumes nodal solution is a displacement array from a modal solution.
16001622
1623+ Parameters
1624+ ----------
16011625 rnum : int or list
16021626 Cumulative result number with zero based indexing, or a
16031627 list containing (step, substep) of the requested result.
@@ -1617,8 +1641,8 @@ def animate_nodal_solution(
16171641 Shows the phase at each frame.
16181642
16191643 add_text : bool, optional
1620- Includes result information at the bottom left-hand corner
1621- of the plot .
1644+ Includes result information at the top left-hand corner of the
1645+ plot. Set font size with the ``font_size`` parameter .
16221646
16231647 interpolate_before_map : bool, optional
16241648 Leaving this at default generally results in a better plot.
@@ -1629,9 +1653,27 @@ def animate_nodal_solution(
16291653 A single loop of the mode will be recorded.
16301654
16311655 kwargs : optional keyword arguments, optional
1632- See help(pyvista.plot) for additional keyword arguments.
1656+ See :func:`pyvista.plot` for additional keyword arguments.
1657+
1658+ Examples
1659+ --------
1660+ Generate a movie of a mode shape while plotting off-screen.
1661+
1662+ >>> from ansys.mapdl.reader import read_binary
1663+ >>> rst = read_binary("academic_rotor.rst")
1664+ >>> rst.animate_nodal_displacement(
1665+ ... (3, 2),
1666+ ... displacement_factor=0.02,
1667+ ... movie_filename="movie.mp4",
1668+ ... off_screen=True
1669+ ... )
16331670
16341671 """
1672+ # Avoid infinite while loop by ensure looping is disabled if off screen
1673+ # and writing a movie
1674+ if movie_filename and kwargs .get ("off_screen" , False ):
1675+ loop = False
1676+
16351677 if "nangles" in kwargs : # pragma: no cover
16361678 n_frames = kwargs .pop ("nangles" )
16371679 warnings .warn (
@@ -1675,6 +1717,7 @@ def animate_nodal_solution(
16751717 scalars = (complex_disp * complex_disp ).sum (1 ) ** 0.5
16761718
16771719 # initialize plotter
1720+ font_size = kwargs .pop ("font_size" , 16 )
16781721 text_color = kwargs .pop ("text_color" , None )
16791722 cpos = kwargs .pop ("cpos" , None )
16801723 off_screen = kwargs .pop ("off_screen" , None )
@@ -1697,9 +1740,8 @@ def animate_nodal_solution(
16971740
16981741 # setup text
16991742 if add_text :
1700- text_actor = plotter .add_text (
1701- " " , font_size = 20 , position = [0 , 0 ], color = text_color
1702- )
1743+ # results in a corner annotation actor
1744+ text_actor = plotter .add_text (" " , font_size = font_size , color = text_color )
17031745
17041746 if cpos :
17051747 plotter .camera_position = cpos
@@ -1740,8 +1782,9 @@ def q_callback():
17401782 plot_mesh .points [:] = orig_pt + complex_disp_adj
17411783
17421784 if add_text :
1743- text_actor .SetInput (
1744- "%s\n Phase %.1f Degrees" % (result_info , (angle * 180 / np .pi ))
1785+ text_actor .set_text (
1786+ 2 , # place in the upper left
1787+ f"{ result_info } \n Phase { np .rad2deg (angle ):.1f} Degrees" ,
17451788 )
17461789
17471790 plotter .update (1 , force_redraw = True )
@@ -1771,7 +1814,7 @@ def _gen_full_rotor(self):
17711814 cs_cord = self ._resultheader ["csCord" ]
17721815 if cs_cord > 1 :
17731816 matrix = self .cs_4x4 (cs_cord , as_vtk_matrix = True )
1774- grid .transform (matrix )
1817+ grid .transform (matrix , inplace = True )
17751818
17761819 # consider forcing low and high to be exact
17771820 # self._mas_grid.point_data['CYCLIC_M01H'] --> rotate and match
@@ -1794,7 +1837,7 @@ def _gen_full_rotor(self):
17941837
17951838 if cs_cord > 1 :
17961839 matrix .Invert ()
1797- full_rotor .transform (matrix )
1840+ full_rotor .transform (matrix , inplace = True )
17981841
17991842 return full_rotor
18001843
@@ -2001,3 +2044,212 @@ def _plot_cyclic_point_scalars(
20012044 cpos = plotter .show (window_size = window_size , full_screen = full_screen )
20022045
20032046 return cpos
2047+
2048+ def save_as_vtk (
2049+ self ,
2050+ filename ,
2051+ rsets = None ,
2052+ result_types = ["ENS" ],
2053+ progress_bar = True ,
2054+ expand_cyclic = True ,
2055+ merge_sectors = True ,
2056+ ):
2057+ """Writes results to a vtk readable file.
2058+
2059+ Nodal results will always be written.
2060+
2061+ The file extension will select the type of writer to use.
2062+ ``'.vtk'`` will use the legacy writer, while ``'.vtu'`` will
2063+ select the VTK XML writer.
2064+
2065+ Parameters
2066+ ----------
2067+ filename : str, pathlib.Path
2068+ Filename of grid to be written. The file extension will
2069+ select the type of writer to use. ``'.vtk'`` will use the
2070+ legacy writer, while ``'.vtu'`` will select the VTK XML
2071+ writer.
2072+
2073+ rsets : collections.Iterable
2074+ List of result sets to write. For example ``range(3)`` or
2075+ [0].
2076+
2077+ result_types : list
2078+ Result type to write. For example ``['ENF', 'ENS']``
2079+ List of some or all of the following:
2080+
2081+ - EMS: misc. data
2082+ - ENF: nodal forces
2083+ - ENS: nodal stresses
2084+ - ENG: volume and energies
2085+ - EGR: nodal gradients
2086+ - EEL: elastic strains
2087+ - EPL: plastic strains
2088+ - ECR: creep strains
2089+ - ETH: thermal strains
2090+ - EUL: euler angles
2091+ - EFX: nodal fluxes
2092+ - ELF: local forces
2093+ - EMN: misc. non-sum values
2094+ - ECD: element current densities
2095+ - ENL: nodal nonlinear data
2096+ - EHC: calculated heat generations
2097+ - EPT: element temperatures
2098+ - ESF: element surface stresses
2099+ - EDI: diffusion strains
2100+ - ETB: ETABLE items
2101+ - ECT: contact data
2102+ - EXY: integration point locations
2103+ - EBA: back stresses
2104+ - ESV: state variables
2105+ - MNL: material nonlinear record
2106+
2107+ progress_bar : bool, optional
2108+ Display a progress bar using ``tqdm``.
2109+
2110+ expand_cyclic : bool, default: True.
2111+ When ``True``, expands cyclic results by writing out the result as
2112+ a full cyclic result rather than as a single cyclic sector.
2113+
2114+ merge_sectors : bool, default: False
2115+ When ``expand_cyclic`` is True and this parameter is ``True``,
2116+ sectors will be merged to create one unified grid. Set this to
2117+ ``False`` to not merge nodes between sectors.
2118+
2119+ Notes
2120+ -----
2121+ Nodal solutions are stored within the ``point_data`` attribute of the
2122+ unstructured grid and can be accessed after reading in the result with
2123+ pyvista with:
2124+
2125+ .. code::
2126+
2127+ >>> grid.point_data
2128+ pyvista DataSetAttributes
2129+ Association : POINT
2130+ Active Scalars : Nodal stresses (0, -2)-2
2131+ Active Vectors : None
2132+ Active Texture : None
2133+ Active Normals : None
2134+ Contains arrays :
2135+ Nodal solution (0, 0) float64 (18864, 3)
2136+ Nodal stresses (0, 0) float64 (18864, 6)
2137+ Nodal solution (1, 0) float64 (18864, 3)
2138+ Nodal stresses (1, 0) float64 (18864, 6)
2139+ Nodal solution (0, -1) float64 (18864, 3)
2140+ Nodal stresses (0, -1) float64 (18864, 6)
2141+ Nodal solution (0, 1) float64 (18864, 3)
2142+ Nodal stresses (0, 1) float64 (18864, 6)
2143+
2144+ See the examples section for more details.
2145+
2146+ Examples
2147+ --------
2148+ Write nodal results as a binary vtk file. Larger file size, loads quickly.
2149+
2150+ >>> rst.save_as_vtk('results.vtk')
2151+
2152+ Write using the xml writer. This file is more compressed compressed but
2153+ will load slower.
2154+
2155+ >>> rst.save_as_vtk('results.vtu')
2156+
2157+ Write only nodal and elastic strain for the first result:
2158+
2159+ >>> rst.save_as_vtk('results.vtk', [0], ['EEL', 'EPL'])
2160+
2161+ Write only nodal results (i.e. displacements) for the first result:
2162+
2163+ >>> rst.save_as_vtk('results.vtk', [0], [])
2164+
2165+ Read in the results using ``pyvista.read()``. Plot the 'Z' component of
2166+ the first mode's -2 nodal diameter nodal displacement.
2167+
2168+ >>> import pyvista as pv
2169+ >>> grid = pv.read('results.vtk')
2170+ >>> grid.plot(scalars="Nodal solution (0, -2)", component=2)
2171+
2172+ Do not merge sectors when saving the results and separate sectors into
2173+ multiple blocks within pyvista.
2174+
2175+ >>> rst.save_as_vtk('results.vtk', merge_sectors=False)
2176+ >>> grid = pv.read('results.vtk')
2177+ >>> mblock = grid.split_bodies()
2178+
2179+ """
2180+
2181+ if rsets is None :
2182+ rsets = range (self .nsets )
2183+ elif isinstance (rsets , int ):
2184+ rsets = [rsets ]
2185+ elif not isinstance (rsets , Iterable ):
2186+ raise TypeError ("rsets must be an iterable like [0, 1, 2] or range(3)" )
2187+
2188+ if result_types is None :
2189+ result_types = ELEMENT_INDEX_TABLE_KEYS
2190+ elif not isinstance (result_types , list ):
2191+ raise TypeError ("result_types must be a list of solution types" )
2192+ else :
2193+ for item in result_types :
2194+ if item not in ELEMENT_INDEX_TABLE_KEYS :
2195+ raise ValueError (f'Invalid result type "{ item } "' )
2196+
2197+ pbar = None
2198+ if progress_bar :
2199+ pbar = tqdm (total = len (rsets ), desc = "Saving to file" )
2200+
2201+ # Copy grid as to not write results to original object
2202+ if not expand_cyclic :
2203+ super ().save_as_vtk (filename , rsets , result_types , progress_bar )
2204+
2205+ # generate the full rotor with separate sectors
2206+ grid = self ._gen_full_rotor ()
2207+ grid .cell_data .pop ("vtkOriginalCellIds" , None )
2208+
2209+ ansys_node_num = None
2210+ if "ansys_node_num" in grid .point_data :
2211+ ansys_node_num = grid .point_data .pop ("ansys_node_num" )
2212+
2213+ grid .point_data .clear ()
2214+ if ansys_node_num is not None :
2215+ grid .point_data ["ansys_node_num" ] = ansys_node_num
2216+
2217+ for i in rsets :
2218+ # convert the result number to a harmonic index and mode number
2219+ mode , hindex = self .mode_table [i ], self .harmonic_indices [i ]
2220+ sol_name = f"({ mode } , { hindex } )"
2221+
2222+ # Nodal results
2223+ # NOTE: val is shaped (n_blades, n_nodes_sector, 3)
2224+ _ , val = self .nodal_solution (i , phase = 0 , full_rotor = True , as_complex = False )
2225+ grid .point_data [f"Nodal solution { sol_name } " ] = np .vstack (val )
2226+
2227+ # Nodal results
2228+ for rtype in self .available_results :
2229+ if rtype in result_types :
2230+
2231+ def sector_func (rnum ):
2232+ return self ._nodal_result (rnum , rtype )
2233+
2234+ _ , values = self ._get_full_result (
2235+ i ,
2236+ sector_func ,
2237+ 0 ,
2238+ True ,
2239+ False ,
2240+ tensor = rtype in RESULT_TENSORS_TYPES ,
2241+ stress = rtype in RESULT_STRESS_TYPES ,
2242+ )
2243+
2244+ desc = element_index_table_info [rtype ]
2245+ grid .point_data [f"{ desc } { sol_name } " ] = np .vstack (values )
2246+
2247+ if pbar is not None :
2248+ pbar .update (1 )
2249+
2250+ if merge_sectors :
2251+ grid = grid .clean (tolerance = 1e-6 )
2252+
2253+ grid .save (str (filename ))
2254+ if pbar is not None :
2255+ pbar .close ()
0 commit comments