Skip to content

Commit 2a49b6a

Browse files
committed
Merge branch 'main' into release/0.55
2 parents 44917db + 251f692 commit 2a49b6a

File tree

3 files changed

+329
-30
lines changed

3 files changed

+329
-30
lines changed

ansys/mapdl/reader/cyclic_reader.py

Lines changed: 265 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,47 @@
11
"""Supports reading cyclic structural result files from ANSYS"""
22

3+
from collections.abc import Iterable
34
from functools import wraps
45
import warnings
56

67
import numpy as np
78
import pyvista as pv
9+
from tqdm import tqdm
810
from vtkmodules.vtkCommonMath import vtkMatrix4x4
911
from vtkmodules.vtkCommonTransforms import vtkTransform
1012
from vtkmodules.vtkFiltersCore import vtkAppendFilter
1113

1214
from ansys.mapdl.reader import _binary_reader
15+
from ansys.mapdl.reader._rst_keys import element_index_table_info
1316
from 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

2225
np.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

2546
class 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\nPhase %.1f Degrees" % (result_info, (angle * 180 / np.pi))
1785+
text_actor.set_text(
1786+
2, # place in the upper left
1787+
f"{result_info}\nPhase {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

Comments
 (0)