Skip to content

Commit

Permalink
Add an associated displacement workflow to a DataFrame (#309)
Browse files Browse the repository at this point in the history
* Implement DataFrame._disp_wf to associate a lazy displacement evaluation for plot and animate

* Expose dpf.core.common.shell_layers as dpf.post.shell_layers

* Improve type-hinting for DataFrame.columns and DataFrame.index

* Add shell_layer selection to DataFrame.animate, as well as merging of shell and solid fields. Reciprocate for displacement workflow.

* Improve test_dataframe_animate

* Improve DataFrame.animate docstring
  • Loading branch information
PProfizi committed Mar 8, 2023
1 parent 8f11142 commit fdfa625
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 36 deletions.
2 changes: 1 addition & 1 deletion src/ansys/dpf/post/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
AvailableServerContexts,
set_default_server_context,
)
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,11 +7,10 @@
import warnings

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

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 @@ -66,16 +65,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 @@ -88,8 +86,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 @@ -631,7 +629,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 @@ -686,7 +684,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 @@ -705,9 +703,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 @@ -718,27 +721,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 @@ -124,12 +124,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

0 comments on commit fdfa625

Please sign in to comment.