Skip to content

Commit 251f692

Browse files
Add cyclic writer (#581)
* add cyclic writer * force transform to inplace --------- Co-authored-by: Roberto Pastor Muela <[email protected]>
1 parent e578f6c commit 251f692

File tree

3 files changed

+274
-12
lines changed

3 files changed

+274
-12
lines changed

ansys/mapdl/reader/cyclic_reader.py

Lines changed: 233 additions & 3 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"""
@@ -1793,7 +1814,7 @@ def _gen_full_rotor(self):
17931814
cs_cord = self._resultheader["csCord"]
17941815
if cs_cord > 1:
17951816
matrix = self.cs_4x4(cs_cord, as_vtk_matrix=True)
1796-
grid.transform(matrix)
1817+
grid.transform(matrix, inplace=True)
17971818

17981819
# consider forcing low and high to be exact
17991820
# self._mas_grid.point_data['CYCLIC_M01H'] --> rotate and match
@@ -1816,7 +1837,7 @@ def _gen_full_rotor(self):
18161837

18171838
if cs_cord > 1:
18181839
matrix.Invert()
1819-
full_rotor.transform(matrix)
1840+
full_rotor.transform(matrix, inplace=True)
18201841

18211842
return full_rotor
18221843

@@ -2023,3 +2044,212 @@ def _plot_cyclic_point_scalars(
20232044
cpos = plotter.show(window_size=window_size, full_screen=full_screen)
20242045

20252046
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()

ansys/mapdl/reader/rst.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3521,7 +3521,11 @@ def plot_nodal_stress(
35213521
)
35223522

35233523
def save_as_vtk(
3524-
self, filename, rsets=None, result_types=["ENS"], progress_bar=True
3524+
self,
3525+
filename,
3526+
rsets=None,
3527+
result_types=["ENS"],
3528+
progress_bar=True,
35253529
):
35263530
"""Writes results to a vtk readable file.
35273531
@@ -3576,12 +3580,6 @@ def save_as_vtk(
35763580
progress_bar : bool, optional
35773581
Display a progress bar using ``tqdm``.
35783582
3579-
Notes
3580-
-----
3581-
Binary files write much faster than ASCII, but binary files
3582-
written on one system may not be readable on other systems.
3583-
Binary can only be selected for the legacy writer.
3584-
35853583
Examples
35863584
--------
35873585
Write nodal results as a binary vtk file.
@@ -3601,8 +3599,6 @@ def save_as_vtk(
36013599
>>> rst.save_as_vtk('results.vtk', [0], [])
36023600
36033601
"""
3604-
# Copy grid as to not write results to original object
3605-
grid = self.quadgrid.copy()
36063602

36073603
if rsets is None:
36083604
rsets = range(self.nsets)
@@ -3624,6 +3620,9 @@ def save_as_vtk(
36243620
if progress_bar:
36253621
pbar = tqdm(total=len(rsets), desc="Saving to file")
36263622

3623+
# Copy grid as to not write results to original object
3624+
grid = self.quadgrid.copy()
3625+
36273626
for i in rsets:
36283627
# Nodal results
36293628
_, val = self.nodal_solution(i)

tests/test_cyclic.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import numpy as np
2828
import pytest
29+
import pyvista as pv
2930
from pyvista.plotting import system_supports_plotting
3031
from pyvista.plotting.renderer import CameraPosition
3132
from vtkmodules.vtkCommonMath import vtkMatrix4x4
@@ -538,3 +539,35 @@ def test_animate_academic(academic_rotor):
538539
add_text=False,
539540
show_scalar_bar=False,
540541
)
542+
543+
544+
def test_save_as_vtk(academic_rotor, tmpdir):
545+
filename = tmpdir.mkdir("tmpdir").join("tmp.vtk")
546+
academic_rotor.save_as_vtk(filename, merge_sectors=False)
547+
grid = pv.read(filename)
548+
549+
# verify for nodal diameter 0
550+
551+
_, disp = academic_rotor.nodal_solution((1, 1), full_rotor=True)
552+
np.allclose(grid["Nodal solution (0, 0)"], np.vstack(disp))
553+
554+
_, stress = academic_rotor.nodal_stress((1, 1), full_rotor=True)
555+
np.allclose(grid["Nodal stresses (0, 0)"], np.vstack(stress))
556+
557+
# verify for nodal diameter 2
558+
559+
_, disp = academic_rotor.nodal_solution((3, 1), full_rotor=True)
560+
np.allclose(grid["Nodal solution (0, -2)"], np.vstack(disp))
561+
562+
_, stress = academic_rotor.nodal_stress((3, 1), full_rotor=True)
563+
np.allclose(grid["Nodal stresses (0, -2)"], np.vstack(stress))
564+
565+
# verify sectors are isolated
566+
assert len(grid.split_bodies()) == academic_rotor.n_sector
567+
568+
# merge, save, and read back in
569+
academic_rotor.save_as_vtk(filename, merge_sectors=True, progress_bar=False)
570+
grid = pv.read(filename)
571+
572+
# verify sectors are not isolated
573+
assert len(grid.split_bodies()) == 1

0 commit comments

Comments
 (0)