Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an associated displacement workflow to a DataFrame #309

Merged
merged 6 commits into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/ansys/dpf/post/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"""

import ansys.dpf.core as core
from ansys.dpf.core.common import locations # noqa: F401
from ansys.dpf.core.common import locations, shell_layers # noqa: F401

try:
import importlib.metadata as importlib_metadata
Expand Down
91 changes: 72 additions & 19 deletions src/ansys/dpf/post/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
import warnings

import ansys.dpf.core as dpf
from ansys.dpf.core.common import shell_layers
from ansys.dpf.core.dpf_array import DPFArray

from ansys.dpf.post import locations
from ansys.dpf.post import locations, shell_layers
from ansys.dpf.post.index import (
CompIndex,
Index,
Expand Down Expand Up @@ -65,16 +64,15 @@ def __init__(
else:
self._columns = None

# if parent_simulation is not None:
# self._parent_simulation = weakref.ref(parent_simulation)
self._disp_wf = None

self._str = None
self._last_display_width = display_width
self._last_display_max_colwidth = display_max_colwidth

@property
def columns(self):
"""Returns the column labels of the DataFrame."""
def columns(self) -> MultiIndex:
"""Returns the MultiIndex for the columns of the DataFrame."""
if self._columns is None:
indexes = [ResultsIndex(values=[self._fc[0].name.split("_")])]
indexes.extend(
Expand All @@ -87,8 +85,8 @@ def columns(self):
return self._columns

@property
def index(self) -> Union[MultiIndex, Index]:
"""Returns the Index or MultiIndex for the rows of the DataFrame."""
def index(self) -> MultiIndex:
"""Returns the MultiIndex for the rows of the DataFrame."""
return self._index

@property
Expand Down Expand Up @@ -619,7 +617,7 @@ def plot(self, shell_layer=shell_layers.top, **kwargs):
Parameters
----------
shell_layer:
Shell layer to show if multi-layered shell data present. Defaults to top.
Shell layer to show if multi-layered shell data is present. Defaults to top.
**kwargs:
This function accepts as argument any of the Index names available associated with a
single value.
Expand Down Expand Up @@ -674,7 +672,7 @@ def plot(self, shell_layer=shell_layers.top, **kwargs):
if shell_layer is not None:
if not isinstance(shell_layer, shell_layers):
raise TypeError(
"shell_layer attribute must be a core.shell_layers instance."
"shell_layer attribute must be a dpf.shell_layers instance."
)
sl = shell_layer
changeOp.inputs.e_shell_layer.connect(sl.value) # top layers taken
Expand All @@ -693,9 +691,14 @@ def animate(
save_as: Union[PathLike, str, None] = None,
deform: bool = False,
scale_factor: Union[List[float], float] = 1.0,
shell_layer: shell_layers = shell_layers.top,
**kwargs,
):
"""Animate the result.
"""Animate the DataFrame along its 'set' axis.

.. note::
At the moment only useful to produce a temporal animation. Each frame will correspond
to data for a value of the SetIndex in the columns MultiIndex.

Parameters
----------
Expand All @@ -706,27 +709,77 @@ def animate(
scale_factor : float, list, optional
Scale factor to apply when warping the mesh. Defaults to 1.0. Can be a list to make
scaling frequency-dependent.
shell_layer:
Shell layer to show if multi-layered shell data is present. Defaults to top.

Returns
-------
The interactive plotter object used for animation.
"""
deform_by = None
if deform:
try:
simulation = self._parent_simulation()
deform_by = simulation._model.results.displacement.on_time_scoping(
self._fc.get_time_scoping()
)
except Exception as e:
if self._disp_wf is None:
warnings.warn(
UserWarning(
"Displacement result unavailable, "
f"unable to animate on the deformed mesh:\n{e}"
f"unable to animate on the deformed mesh."
)
)
else:
wf = dpf.workflow.Workflow()
forward_op = dpf.operators.utility.forward_fields_container(
server=self._fc._server
)
wf.add_operator(forward_op)
wf.set_input_name("input", forward_op.inputs.fields)
output_input_names = ("output", "input")
wf.connect_with(
left_workflow=self._disp_wf, output_input_names=output_input_names
)

deform_by = forward_op
else:
deform_by = False
return self._fc.animate(

fc = self._fc

# Modify fc to merge fields of different eltype at same time and to select a shell_layer
# (until it is done on core-side -> animation feature to refactor)

sl = shell_layers.top
# Select shell_layer
for field in fc:
# Treat multi-layer field
shell_layer_check = field.shell_layers
if shell_layer_check in [
shell_layers.topbottom,
shell_layers.topbottommid,
]:
if shell_layer is not None:
if not isinstance(shell_layer, shell_layers):
raise TypeError(
"shell_layer attribute must be a dpf.shell_layers instance."
)
sl = shell_layer
shell_layer_op = dpf.operators.utility.change_shell_layers(
fields_container=fc,
server=fc._server,
e_shell_layer=sl.value(),
)
fc = shell_layer_op.outputs.fields_container_as_fields_container()
break

# Set shell layer input for self._disp_wf
self._disp_wf.connect("shell_layer_int", sl.value)

# Merge shell and solid fields at same set
if "eltype" in fc.labels:
merge_op = dpf.operators.utility.merge_fields_by_label(
fields_container=fc,
label="eltype",
)
fc = merge_op.outputs.fields_container()

return fc.animate(
save_as=save_as, deform_by=deform_by, scale_factor=scale_factor, **kwargs
)
6 changes: 5 additions & 1 deletion src/ansys/dpf/post/harmonic_mechanical_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ def _get_result(
# Evaluate the workflow
fc = wf.get_output("out", core.types.fields_container)

disp_wf = self._generate_disp_workflow(fc, selection)

# Test for empty results
if (len(fc) == 0) or all([len(f) == 0 for f in fc]):
warnings.warn(
Expand Down Expand Up @@ -301,11 +303,13 @@ def _get_result(
)

# Return the result wrapped in a DPF_Dataframe
return DataFrame(
df = DataFrame(
data=fc,
columns=column_index,
index=row_index,
)
df._disp_wf = disp_wf
return df

def displacement(
self,
Expand Down
4 changes: 3 additions & 1 deletion src/ansys/dpf/post/modal_mechanical_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ def _get_result(
# Evaluate the workflow
fc = wf.get_output("out", core.types.fields_container)

return self._create_dataframe(fc, location, columns, comp, base_name)
disp_wf = self._generate_disp_workflow(fc, selection)

return self._create_dataframe(fc, location, columns, comp, base_name, disp_wf)

def displacement(
self,
Expand Down
52 changes: 49 additions & 3 deletions src/ansys/dpf/post/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,50 @@ def _create_components(self, base_name, category, components):
raise ValueError(f"'{category}' is not a valid category value.")
return comp, to_extract, columns

def _create_dataframe(self, fc, location, columns, comp, base_name):
def _generate_disp_workflow(self, fc, selection) -> Union[dpf.Workflow, None]:
# Check displacement is an available result
if not any(
[
result.name == "displacement"
for result in self._model.metadata.result_info.available_results
]
):
return None
# Build an equivalent workflow for displacement for plots and animations
disp_wf = dpf.Workflow(server=fc._server)

disp_op = dpf.operators.result.displacement(
data_sources=self._model.metadata.data_sources,
streams_container=self._model.metadata.streams_provider,
server=fc._server,
)
# Connect time_scoping (do not connect mesh_scoping as we want to deform the whole mesh)
disp_wf.set_input_name("time_scoping", disp_op.inputs.time_scoping)
disp_wf.connect_with(
selection.time_freq_selection._selection,
output_input_names=("scoping", "time_scoping"),
)

# Shell layer selection step
shell_layer_op = dpf.operators.utility.change_shell_layers(
fields_container=disp_op.outputs.fields_container,
server=fc._server,
)
# Expose shell layer input as workflow input
disp_wf.set_input_name("shell_layer_int", shell_layer_op.inputs.e_shell_layer)

# Merge shell and solid fields at same set
merge_op = dpf.operators.utility.merge_fields_by_label(
fields_container=shell_layer_op.outputs.fields_container_as_fields_container,
label="eltype",
)

# Expose output
disp_wf.set_output_name("output", merge_op.outputs.fields_container)

return disp_wf

def _create_dataframe(self, fc, location, columns, comp, base_name, disp_wf=None):
# Test for empty results
if (len(fc) == 0) or all([len(f) == 0 for f in fc]):
warnings.warn(
Expand Down Expand Up @@ -493,12 +536,15 @@ def _create_dataframe(self, fc, location, columns, comp, base_name):
indexes=row_indexes,
)

# Return the result wrapped in a DPF_Dataframe
return DataFrame(
df = DataFrame(
data=fc,
columns=column_index,
index=row_index,
)
df._disp_wf = disp_wf

# Return the result wrapped in a DPF_Dataframe
return df


class MechanicalSimulation(Simulation, ABC):
Expand Down
4 changes: 3 additions & 1 deletion src/ansys/dpf/post/static_mechanical_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,9 @@ def _get_result(
# Evaluate the workflow
fc = wf.get_output("out", core.types.fields_container)

return self._create_dataframe(fc, location, columns, comp, base_name)
disp_wf = self._generate_disp_workflow(fc, selection)

return self._create_dataframe(fc, location, columns, comp, base_name, disp_wf)

def displacement(
self,
Expand Down
10 changes: 6 additions & 4 deletions src/ansys/dpf/post/transient_mechanical_simulation.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Module containing the ``TransientMechanicalSimulation`` class."""
from typing import List, Tuple, Union

from ansys.dpf import core
from ansys.dpf import core as dpf
from ansys.dpf.post import locations
from ansys.dpf.post.dataframe import DataFrame
from ansys.dpf.post.selection import Selection
Expand Down Expand Up @@ -114,7 +114,7 @@ def _get_result(
)

# Initialize a workflow
wf = core.Workflow(server=self._model._server)
wf = dpf.Workflow(server=self._model._server)
wf.progress_bar = False

if category == ResultCategory.equivalent and base_name[0] == "E":
Expand Down Expand Up @@ -218,9 +218,11 @@ def _get_result(
# Set the workflow output
wf.set_output_name("out", out)
# Evaluate the workflow
fc = wf.get_output("out", core.types.fields_container)
fc = wf.get_output("out", dpf.types.fields_container)

return self._create_dataframe(fc, location, columns, comp, base_name)
disp_wf = self._generate_disp_workflow(fc, selection)

return self._create_dataframe(fc, location, columns, comp, base_name, disp_wf)

def displacement(
self,
Expand Down
11 changes: 5 additions & 6 deletions tests/test_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,11 @@ def test_dataframe_animate(transient_rst):
simulation = TransientMechanicalSimulation(transient_rst)
# Animate displacement
df = simulation.displacement(all_sets=True)
# df.animate()
df.animate(scale_factor=5.0, deform=True, save_as="test_dataframe_animate.gif")
# Animate nodal stress -> Does not work
df2 = simulation.stress_nodal(all_sets=True)
# df2.animate()
# assert False
df.animate()
df.animate(scale_factor=5.0, deform=True)

df = simulation.stress_nodal(all_sets=True)
df.animate(deform=True, scale_factor=5.0)


def test_dataframe_repr(df):
Expand Down