Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
254bc63
bla
rafacanton Mar 12, 2026
baf04a5
bla
rafacanton Mar 12, 2026
d60680e
Revert "bla"
rafacanton Mar 12, 2026
6447f75
Revert "bla"
rafacanton Mar 12, 2026
7e642f5
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 12, 2026
3ab8ca4
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 13, 2026
176ac1c
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 13, 2026
95e05de
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 16, 2026
d3d45d4
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 16, 2026
823961f
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 19, 2026
8eefde8
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 23, 2026
aadd9b7
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Mar 24, 2026
9c2016f
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 6, 2026
39c73fa
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 8, 2026
984ae94
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 13, 2026
0c001d9
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 13, 2026
3d53197
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 16, 2026
415c8e6
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 20, 2026
9ddb175
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 20, 2026
7a5c3b7
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 21, 2026
68e98a0
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 21, 2026
b04fcb6
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 24, 2026
3489b56
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 27, 2026
48b27cb
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 27, 2026
1c5022e
Merge branch 'main' of https://github.com/ansys/pydpf-core
rafacanton Apr 29, 2026
b52a021
Add tutorial to understand cyclic models
rafacanton Apr 29, 2026
1d3180d
Merge branch 'main' into rcanton/tuto_cyclic
rafacanton Apr 30, 2026
4f797a7
Merge branch 'main' into rcanton/tuto_cyclic
rafacanton May 4, 2026
a735ca6
Merge branch 'main' into rcanton/tuto_cyclic
PProfizi May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/sphinx_gallery_tutorials/import_data/GALLERY_HEADER.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ from simulation result files.
Use a streams container to avoid redundant file I/O when requesting
multiple results from the same result file.

.. grid-item-card:: Understand cyclic expansion of results
:link: ref_tutorials_import_data_cyclic_expansion
:link-type: ref
:text-align: center

Learn how DPF handles cyclic symmetry data and the different cyclic expansion modes.

+++
:bdg-mapdl:`MAPDL`

.. raw:: html

<style>.sphx-glr-thumbnails { display: none; }</style>
248 changes: 248 additions & 0 deletions doc/sphinx_gallery_tutorials/import_data/cyclic_expansion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# Copyright (C) 2020 - 2026 ANSYS, Inc. and/or its affiliates.
# SPDX-License-Identifier: MIT
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# _order: 7
"""
.. _ref_tutorials_import_data_cyclic_expansion:

Understand cyclic expansion of results
=======================================

:bdg-mapdl:`MAPDL`

Learn how DPF handles cyclic symmetry data and the different cyclic expansion modes.

When a model possesses cyclic symmetry, only one sector (the base sector) is solved.
DPF can read these results in different ways depending on the ``read_cyclic`` option
passed to a result operator. This tutorial demonstrates the four modes available
(``0``, ``1``, ``2``, ``3``) and shows how to perform phase sweeping on the expanded
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rafacanton did you choose to showcase a phase sweep because this is what people usually do?
I fear that by focusing on phase sweeps we may loose the generic topic of dealing with cyclic symmetries.
Performing phase sweeps could be in another tutorial.
Or are we performing one simply because the only cyclic example we have is this one and it requires a phase sweep to show the expanded displacement field?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have preferred to showcase how to extract expanded results and use them along with expanded/non-expanded meshes.

Copy link
Copy Markdown
Contributor

@PProfizi PProfizi May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is that this is very harmonic-frequency-analysis-oriented while the goal of the tutorial is to show how to deal with cyclic models in general.

results.
"""
###############################################################################
# Import modules and load the model
# ----------------------------------
#
# Import the required modules and create a |Model| from the built-in cyclic
# symmetry example file.

# Import the ansys.dpf.core module
from ansys.dpf import core as dpf

# Import the examples module
from ansys.dpf.core import examples

# Get the path to the cyclic result file
result_url = examples.download_modal_cyclic_complex()

# Create the Model
my_model = dpf.Model(data_sources=result_url)
print(my_model)

###############################################################################
# Explore the cyclic metadata
# ----------------------------
#
# A cyclic model exposes a
# :class:`CyclicSupport<ansys.dpf.core.cyclic_support.CyclicSupport>` that
# provides the number of sectors, node counts on the base sector, and the
# mapping between low and high boundary nodes.
#
# The |TimeFreqSupport| describes how solution sets map to harmonic indices and
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This too is specific to frequency domain analyses, but we are presenting the CyclicSupport.

# mode numbers.

# Get the TimeFreqSupport. In this model we have a damped, cyclic, modal analysis.
# Therefore, mode shapes are complex. The model spans 45º, therefore there are 8
# sectors and harmonic indices range from 0 to 4. The MAPDL analysis has been run
# ensuring that we extract 4 cyclic modes per harmonic index and all harmonic
# indices, this means 5*4 = 20 solution sets, so 10 real-imaginary combinations.
# In the print of the TimeFreqSupport we can see the 20 solution sets, and by default
# only the imaginary part of the frequency is printed. Each LoadStep is a harmonic
# index and each substep represents a cyclic mode for each one of them. For both
# harmonic indices 0 and 4, modes are standalone, whereas for the intermediate
# indices modes come in pairs.
tfs = my_model.metadata.time_freq_support
print(tfs)

###############################################################################
# Get the CyclicSupport from the ResultInfo. The CyclicSupport allows to understand
# the cyclic information of the model. We can see that we have only 1 stage with 8
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any example with several stages?
If the goal of the tutorial is to present the CyclicSupport, which happens to store information about stages, maybe we need a better example.

# sectors (45º), 3657 nodes on each sector and high/low boundaries with 217 nodes.

cyc_sup = my_model.metadata.result_info.cyclic_support
print(cyc_sup)

# Print the number of stages and sectors
print(f"Number of stages: {cyc_sup.num_stages}")
print(f"Number of sectors (stage 0): {cyc_sup.num_sectors(stage_num=0)}")

# Print the number of boundary nodes (low-high map)
low_high = cyc_sup.low_high_map(stage_num=0)
print(f"Number of boundary nodes: {len(low_high.scoping)}")

###############################################################################
# Read cyclic mode 0 - ignore cyclic symmetry
# ---------------------------------------------
#
# With ``read_cyclic=0``, cyclic symmetry is completely ignored. DPF reads the
# base and duplicate sector data together into a single |Field| per solution set.
# Each Field contains data for all nodes stored in the file (base + duplicate
# sector, hence each Field has information for 3657*2 = 7314 nodes). Results are
# extracted at all solution sets. As the solutions are complex, and the information
# in the time scoping pin represents a cumulative id, if we want to read all we
# need to pass 1, 2, ... 20/2 ids.

# Create the displacement X operator
disp_op = dpf.operators.result.displacement_X()
disp_op.inputs.streams_container.connect(my_model.metadata.streams_provider)
disp_op.inputs.read_cyclic.connect(0)
disp_op.inputs.time_scoping.connect(list(range(1, len(tfs.time_frequencies) // 2 + 1)))
disp_rc0 = disp_op.outputs.fields_container()

print("read_cyclic=0:")
print(disp_rc0)

###############################################################################
# Read cyclic mode 1 - read as cyclic without expansion (default)
# ----------------------------------------------------------------
#
# With ``read_cyclic=1`` (the default), DPF reads the data as cyclic and splits
# results into base sector (``base_sector=1``) and duplicate sector
# (``base_sector=0``) Fields, hence, each Field has 3657 nodes. Harmonic indices
# without a duplicate sector (e.g., first HI = 0) only have the base sector Field,
# we have then 10*2*2 - 2*2*2 = 32 Fields (we lack the 8 combinations between
# base_sector = 0 and time = 1,2,3,4.

# Set read_cyclic to 1
disp_op.inputs.read_cyclic.connect(1)
disp_rc1 = disp_op.outputs.fields_container()

print("read_cyclic=1:")
print(disp_rc1)

###############################################################################
# Read cyclic mode 2 - expand without merging stages
# ----------------------------------------------------
#
# With ``read_cyclic=2``, DPF performs the full cyclic expansion. The result
# Fields now contain the data for all sectors assembled into the full 360-degree
# model. Stages are kept separate (relevant for multi-stage models). In this
# case we obtain 10*2 Fields after collapsing the base_sector label. The base
# sector has 3657 nodes, and the low/high boundaries have 217 nodes, therefore
# each field has 3657 + 6*(3657 - 217) + (3657 - 2*217) = 27520 nodes.

# Set read_cyclic to 2
disp_op.inputs.read_cyclic.connect(2)
disp_rc2 = disp_op.outputs.fields_container()

print("read_cyclic=2:")
print(disp_rc2)

###############################################################################
# Read cyclic mode 3 - expand and merge stages
# ----------------------------------------------
#
# With ``read_cyclic=3``, the cyclic expansion is done and all stages are merged
# into a single mesh/result. For single-stage models as this one, this produces
# the same output as ``read_cyclic=2``.

# Set read_cyclic to 3
disp_op.inputs.read_cyclic.connect(3)
disp_rc3 = disp_op.outputs.fields_container()

print("read_cyclic=3:")
print(disp_rc3)

###############################################################################
# Compare Field sizes across modes
# ----------------------------------
#
# The following comparison shows how the number of entities grows with each
# expansion mode.

print(f"read_cyclic=0, first field entities: {len(disp_rc0[0])}")
print(f"read_cyclic=1, first field entities: {len(disp_rc1[0])}")
print(f"read_cyclic=2, first field entities: {len(disp_rc2[0])}")
print(f"read_cyclic=3, first field entities: {len(disp_rc3[0])}")

###############################################################################
# Phase sweeping on expanded results
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so it's not that this is not great and I don't want it here, it's more that I think it deserves its own tutorial so that people can find this information better.

# ------------------------------------
#
# For complex mode shapes (modal cyclic analysis), you can perform a phase sweep
# to obtain results at any phase angle. The expression used is:
#
# .. math::
#
# \text{field\_out} = \text{real\_field} \cdot \cos(\theta)
# - \text{imaginary\_field} \cdot \sin(\theta)
#
# The ``sweeping_phase_fc`` operator applies this to a |FieldsContainer| that
# contains paired real and imaginary Fields (identified by the ``complex`` label).
#
# A sweep at :math:`\theta = 0º` recovers the real part, while
# :math:`\theta = -90º` recovers the imaginary part.

# Create the sweeping_phase_fc operator
sweep_op = dpf.operators.math.sweeping_phase_fc()
sweep_op.inputs.fields_container.connect(disp_rc2)
sweep_op.inputs.angle.connect(0.0)
sweep_op.inputs.unit_name.connect("deg")

# Evaluate at 0 degrees (real part)
disp_sweep_0 = sweep_op.outputs.fields_container()
print("Phase sweep at 0 degrees:")
print(disp_sweep_0)

###############################################################################
# Sweep at -90 degrees to recover the imaginary part.

sweep_op.inputs.angle.connect(-90.0)
disp_sweep_90 = sweep_op.outputs.fields_container()
print("Phase sweep at -90 degrees:")
print(disp_sweep_90)

###############################################################################
# Visualize expanded results
# ---------------------------
#
# To visualize the expanded displacement, obtain the expanded mesh by setting
# ``read_cyclic=2`` on the mesh provider. Then plot the first expanded mode
# shape on the full 360-degree mesh.

# Get the expanded mesh
mesh_provider = my_model.metadata.mesh_provider
mesh_provider.inputs.read_cyclic(2)
expanded_mesh = mesh_provider.outputs.mesh()

# Plot the first mode (real part) on the expanded mesh
expanded_mesh.plot(disp_rc2[0])

###############################################################################
# Plot a phase-swept result
# ---------------------------
#
# Plot the first mode shape after sweeping at 45 degrees.

sweep_op.inputs.angle.connect(45.0)
disp_sweep_45 = sweep_op.outputs.fields_container()

expanded_mesh.plot(disp_sweep_45[0])
40 changes: 40 additions & 0 deletions src/ansys/dpf/core/examples/downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,46 @@ def download_modal_cyclic(should_upload: bool = True, server=None, return_local_
)


def download_modal_cyclic_complex(
should_upload: bool = True, server=None, return_local_path=False
) -> str:
"""Download an example result file from a cyclic modal analysis with complex solutions and return the download path.

If the server is remote (or doesn't share memory), the file is uploaded or made available
on the server side.

Examples files are downloaded to a persistent cache to avoid
re-downloading the same file twice.

Parameters
----------
should_upload : bool, optional (default True)
Whether the file should be uploaded server side when the server is remote.
server : server.DPFServer, optional
Server with channel connected to the remote or local instance. When
``None``, attempts to use the global server.
return_local_path: bool, optional
If ``True``, the local path is returned as is, without uploading, nor searching
for mounted volumes.

Returns
-------
str
Path to the example file.

Examples
--------
Download an example result file and return the path of the file

>>> from ansys.dpf.core import examples
>>> path = examples.download_modal_cyclic_complex()

"""
return _download_file(
"result_files/cyclic", "modal_cyclic_complex.rst", should_upload, server, return_local_path
)


def download_fluent_axial_comp(
should_upload: bool = True, server=None, return_local_path=False
) -> dict:
Expand Down
5 changes: 5 additions & 0 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ def test_download_modal_cyclic():
assert isinstance(Model(path), Model)


def test_download_modal_cyclic_complex():
path = examples.download_modal_cyclic_complex()
assert isinstance(Model(path), Model)


def test_download_fluent_multi_species():
path = examples.download_fluent_multi_species()
assert isinstance(Model(path), Model)
Expand Down
Loading