Skip to content

Commit

Permalink
Add convenience function to get ply-wise data (#396)
Browse files Browse the repository at this point in the history
* Add function to extract ply wise data
* Add test for extract ply wise data
* Update filtering example
  • Loading branch information
janvonrickenbach committed Jan 15, 2024
1 parent 2a82215 commit 945afc9
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 49 deletions.
1 change: 1 addition & 0 deletions doc/source/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ For in-depth documentation on the different failure criteria, refer to the ACP h
data_sources
failure_criteria
layup_info
ply_wise_data
result_definition
sampling_point
server_helpers
Expand Down
12 changes: 12 additions & 0 deletions doc/source/api/ply_wise_data.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Ply wise data
-------------

.. module:: ansys.dpf.composites.ply_wise_data

.. autosummary::
:toctree: _autosummary

ReductionStrategy
get_ply_wise_data


127 changes: 80 additions & 47 deletions examples/006_filter_composite_data_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@
Filter result data by different criteria
----------------------------------------
This example show how data filtering can be used for custom postprocessing of
This example shows how data filtering can be used for custom postprocessing of
layered composites. You can filter strains and stresses by material, layer, or
analysis ply. The example filters data by layer, spot, and node, as well as material
analysis ply. Filtering by analysis ply is implemented on the server side and
exposed with the :func:`.get_ply_wise_data` function. In this case, the data is
filtered (and reduced) on the server side and only the resulting field is returned
to the client. This is the recommended way to filter data if possible.
For more complex filtering, the data is transferred to the client side and filtered
using numpy functionality.
The examples show filtering data by layer, spot, and node, as well as material
or analysis ply ID. To learn more about how layered result data is organized,
see :ref:`select_indices`.
"""
Expand All @@ -24,17 +30,14 @@
from ansys.dpf.composites.composite_model import CompositeModel
from ansys.dpf.composites.constants import Spot, Sym3x3TensorComponent
from ansys.dpf.composites.example_helper import get_continuous_fiber_example_files
from ansys.dpf.composites.layup_info import (
AnalysisPlyInfoProvider,
get_all_analysis_ply_names,
get_dpf_material_id_by_analysis_ply_map,
)
from ansys.dpf.composites.layup_info import AnalysisPlyInfoProvider, get_all_analysis_ply_names
from ansys.dpf.composites.ply_wise_data import ReductionStrategy, get_ply_wise_data
from ansys.dpf.composites.select_indices import (
get_selected_indices,
get_selected_indices_by_analysis_ply,
get_selected_indices_by_dpf_material_ids,
)
from ansys.dpf.composites.server_helpers import connect_to_or_start_server
from ansys.dpf.composites.server_helpers import connect_to_or_start_server, version_equal_or_later

# %%
# Start a DPF server and copy the example files into the current working directory.
Expand All @@ -55,16 +58,52 @@
stress_operator.inputs.bool_rotate_to_global(False)
stress_field = stress_operator.get_output(pin=0, output_type=dpf.types.fields_container)[0]

# %%
# Filter data by analysis ply
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~

# %%
# List all available analysis plies.
all_ply_names = get_all_analysis_ply_names(composite_model.get_mesh())
all_ply_names

# %%
# The easiest way to filter data by analysis ply is to use the :func:`.get_ply_wise_data` function.
# This function supports different reduction strategies such as computing the average,
# maximum, or minimum over the spot locations.
# It also supports selecting a specific spot (TOP, MID, BOT) directly.
# This example selects the maximum value over all spots for each node and then requests
# the elemental location, which implies averaging over all nodes in an element.
# Using the :func:`.get_ply_wise_data` function has the advantage that all the averaging
# and filtering is done on the server side.
if version_equal_or_later(server, "8.0"):
elemental_max = get_ply_wise_data(
field=stress_field,
ply_name="P1L1__ud_patch ns1",
mesh=composite_model.get_mesh(),
component=Sym3x3TensorComponent.TENSOR11,
reduction_strategy=ReductionStrategy.MAX,
requested_location=dpf.locations.elemental,
)

composite_model.get_mesh().plot(elemental_max)


# %%
# Generic client-side filtering
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This example shows how to filter data by layer, spot, and node using the generic filtering on
# the client side.
# This code plots stress values in the material direction for the first node and top spot.

# %%
# Get element information for all elements and show the first one as an example.
element_ids = stress_field.scoping.ids
element_infos = [composite_model.get_element_info(element_id) for element_id in element_ids]
element_infos[0]

# %%
# Plot result data
# ~~~~~~~~~~~~~~~~
# For the top layer, plot stress values in the material direction for the first node and top spot.
# Get filtered data
component = Sym3x3TensorComponent.TENSOR11
result_field = dpf.field.Field(location=dpf.locations.elemental, nature=dpf.natures.scalar)
with result_field.as_local_field() as local_result_field:
Expand All @@ -82,23 +121,45 @@

composite_model.get_mesh().plot(result_field)


# %%
# List analysis plies
# ~~~~~~~~~~~~~~~~~~~
# List all available analysis plies.
all_ply_names = get_all_analysis_ply_names(composite_model.get_mesh())
all_ply_names
# Filter by material
# ~~~~~~~~~~~~~~~~~~~~~
# Loop over all elements and get the maximum stress in the material direction
# for all plies that have a specific UD material.

ud_material_id = composite_model.material_names["Epoxy Carbon UD (230 GPa) Prepreg"]
component = Sym3x3TensorComponent.TENSOR11

material_result_field = dpf.field.Field(location=dpf.locations.elemental, nature=dpf.natures.scalar)
with material_result_field.as_local_field() as local_result_field:
element_ids = stress_field.scoping.ids

for element_id in element_ids:
element_info = composite_model.get_element_info(element_id)
assert element_info is not None
if ud_material_id in element_info.dpf_material_ids:
stress_data = stress_field.get_entity_data_by_id(element_id)
selected_indices = get_selected_indices_by_dpf_material_ids(
element_info, [ud_material_id]
)

value = np.max(stress_data[selected_indices][:, component])
local_result_field.append([value], element_id)

composite_model.get_mesh().plot(material_result_field)

# %%
# Plot results
# ~~~~~~~~~~~~
# Loop all elements that contain a given ply and plot the maximum stress value
# Filter by analysis ply on the client side
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Loop over all elements that contain a given ply and plot the maximum stress value
# in the material direction in this ply.
component = Sym3x3TensorComponent.TENSOR11

analysis_ply_info_provider = AnalysisPlyInfoProvider(
mesh=composite_model.get_mesh(), name="P1L1__ud_patch ns1"
)

ply_result_field = dpf.field.Field(location=dpf.locations.elemental, nature=dpf.natures.scalar)
with ply_result_field.as_local_field() as local_result_field:
element_ids = analysis_ply_info_provider.property_field.scoping.ids
Expand All @@ -116,31 +177,3 @@


composite_model.get_mesh().plot(ply_result_field)

# %%
# Loop all elements and get the maximum stress in the material direction
# for all plies that have a material with DPF material ID.
# Note: It is not possible to get a DPF material ID for a
# given material name. It is only possible to get a DPF material
# ID from an analysis ply.
material_map = get_dpf_material_id_by_analysis_ply_map(
composite_model.get_mesh(), data_source_or_streams_provider=composite_model.data_sources.rst
)
ud_material_id = material_map["P1L1__ud_patch ns1"]
component = Sym3x3TensorComponent.TENSOR11

material_result_field = dpf.field.Field(location=dpf.locations.elemental, nature=dpf.natures.scalar)
with material_result_field.as_local_field() as local_result_field:
element_ids = analysis_ply_info_provider.property_field.scoping.ids

for element_id in element_ids:
stress_data = stress_field.get_entity_data_by_id(element_id)
element_info = composite_model.get_element_info(element_id)
assert element_info is not None

selected_indices = get_selected_indices_by_dpf_material_ids(element_info, [ud_material_id])

value = np.max(stress_data[selected_indices][:, component])
local_result_field.append([value], element_id)

composite_model.get_mesh().plot(material_result_field)
2 changes: 2 additions & 0 deletions src/ansys/dpf/composites/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
data_sources,
failure_criteria,
layup_info,
ply_wise_data,
result_definition,
sampling_point,
select_indices,
Expand All @@ -27,6 +28,7 @@
"data_sources",
"failure_criteria",
"layup_info",
"ply_wise_data",
"result_definition",
"sampling_point",
"server_helpers",
Expand Down
4 changes: 2 additions & 2 deletions src/ansys/dpf/composites/layup_info/_layup_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,8 @@ def get_dpf_material_id_by_analyis_ply_map(
DPF data source with rst file or streams_provider. The streams provider is
available from :attr:`.CompositeModel.core_model` (under metadata.streams_provider).
Note
----
Notes
-----
Cache the output because the computation can be performance-critical.
"""
warn(
Expand Down
87 changes: 87 additions & 0 deletions src/ansys/dpf/composites/ply_wise_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Methods to get ply-wise data from a result field."""

from enum import Enum, IntEnum
from typing import Union

from ansys.dpf.core import Field, MeshedRegion, Operator, operators
from ansys.dpf.gate.common import locations

__all__ = ("ReductionStrategy", "get_ply_wise_data")


class ReductionStrategy(Enum):
"""Provides the reduction strategy for getting from spot values to a single value."""

MIN = "MIN"
MAX = "MAX"
AVG = "AVG"
BOT = "BOT"
MID = "MID"
TOP = "TOP"


def get_ply_wise_data(
field: Field,
ply_name: str,
mesh: MeshedRegion,
reduction_strategy: ReductionStrategy = ReductionStrategy.AVG,
requested_location: str = locations.elemental_nodal,
component: Union[IntEnum, int] = 0,
) -> Field:
"""Get ply-wise data from a field.
Parameters
----------
field:
Field to extract data from.
ply_name:
Name of the ply to extract data from.
mesh :
Meshed region enriched with composite information.
Use the ``CompositeModel.get_mesh()`` method to get the meshed region.
reduction_strategy :
Reduction strategy for getting from spot values (BOT, MID, TOP) to a single value
per corner node and layer. The default is ``AVG``.
requested_location :
Location of the output field. The default is ``"elemental_nodal"``. Options are
``"elemental"``, ``"elemental_nodal"``, and ``"nodal"``.
component :
Component to extract data from. The default is ``0``.
"""
component_int = component.value if isinstance(component, IntEnum) else component
component_selector = operators.logic.component_selector()

component_selector.inputs.field.connect(field)
component_selector.inputs.component_number.connect(component_int)
single_component_field = component_selector.outputs.field()

filter_ply_data_op = Operator("composite::filter_ply_data_operator")
filter_ply_data_op.inputs.field(single_component_field)
filter_ply_data_op.inputs.mesh(mesh)
filter_ply_data_op.inputs.ply_id(ply_name)
filter_ply_data_op.inputs.reduction_strategy(reduction_strategy.value)
elemental_nodal_data = filter_ply_data_op.outputs.field()

if requested_location == locations.elemental_nodal:
return elemental_nodal_data

if requested_location == locations.elemental:
elemental_nodal_to_elemental = operators.averaging.elemental_mean()
elemental_nodal_to_elemental.inputs.field.connect(elemental_nodal_data)
out_field = elemental_nodal_to_elemental.outputs.field()
out_field.location = locations.elemental
return out_field

if requested_location == locations.nodal:
elemental_nodal_to_nodal = operators.averaging.elemental_nodal_to_nodal()
elemental_nodal_to_nodal.inputs.mesh.connect(mesh)
elemental_nodal_to_nodal.inputs.field.connect(elemental_nodal_data)
out_field = elemental_nodal_to_nodal.outputs.field()
out_field.location = locations.nodal
return out_field

raise RuntimeError(
f"Invalid requested location {requested_location}. "
f"Valid locations are {locations.elemental_nodal}, "
f"{locations.elemental}, and {locations.nodal}."
)
Loading

0 comments on commit 945afc9

Please sign in to comment.