diff --git a/doc/source/index.rst b/doc/source/index.rst index c753a1ce6..1f67c93ba 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -74,6 +74,7 @@ Here are some key features of PyDPF Composites: Limitations ''''''''''' - Only the Mechanical APDL solver is supported. +- The post-processing of expanded cyclic symmetry models is not implemented. - The following operators and features are only supported if the model was preprocessed with ACP and if the corresponding lay-up definition file is passed to the :class:`.CompositeModel` class. diff --git a/examples/014_cyclic_symmetry_example.py b/examples/014_cyclic_symmetry_example.py new file mode 100644 index 000000000..b3ee01921 --- /dev/null +++ b/examples/014_cyclic_symmetry_example.py @@ -0,0 +1,159 @@ +# Copyright (C) 2023 - 2024 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. + +""" +.. _cyclic_symmetry_example: + +Cyclic symmetry +--------------- + +This example shows how to postprocess a cyclic symmetry analysis. +The initial (original) sector can be postprocessed with the same tools +as a standard analysis. The postprocessing workflow is demonstrated by +running a failure analysis, extracting ply-wise stresses, and implementing +a custom failure criterion. + +The postprocessing of expanded sectors is not yet supported. +""" + +# %% +# Set up analysis +# ~~~~~~~~~~~~~~~ +# Setting up the analysis consists of loading the required modules, connecting to the +# DPF server, and retrieving the example files. +# +# Load Ansys libraries and helper functions. +import ansys.dpf.core as dpf + +from ansys.dpf.composites.composite_model import CompositeModel +from ansys.dpf.composites.constants import FailureOutput, Sym3x3TensorComponent +from ansys.dpf.composites.example_helper import get_continuous_fiber_example_files +from ansys.dpf.composites.failure_criteria import CombinedFailureCriterion, MaxStressCriterion +from ansys.dpf.composites.layup_info import get_all_analysis_ply_names +from ansys.dpf.composites.layup_info.material_properties import MaterialProperty +from ansys.dpf.composites.ply_wise_data import SpotReductionStrategy, get_ply_wise_data +from ansys.dpf.composites.select_indices import get_selected_indices +from ansys.dpf.composites.server_helpers import connect_to_or_start_server + +# %% +# Start a DPF server and copy the example files into the current working directory. +server = connect_to_or_start_server() +composite_files = get_continuous_fiber_example_files(server, "cyclic_symmetry") + +# %% +# Create a composite model. +composite_model = CompositeModel(composite_files, server) + +# %% +# Evaluate a combined failure criterion. +combined_failure_criterion = CombinedFailureCriterion(failure_criteria=[MaxStressCriterion()]) +failure_result = composite_model.evaluate_failure_criteria(combined_failure_criterion) + +# %% +# Plot the failure results. +irf_field = failure_result.get_field({"failure_label": FailureOutput.FAILURE_VALUE}) +irf_field.plot() + +# %% +# Plot ply-wise stresses +# ~~~~~~~~~~~~~~~~~~~~~~ +# All functions in PyDPF Composites can be used to +# postprocess the initial (original) sector. + +rst_stream = composite_model.core_model.metadata.streams_provider +stress_operator = dpf.operators.result.stress() +stress_operator.inputs.streams_container.connect(rst_stream) +stress_operator.inputs.bool_rotate_to_global(False) +stress_container = stress_operator.outputs.fields_container() + +all_ply_names = get_all_analysis_ply_names(composite_model.get_mesh()) +all_ply_names + +component_s11 = Sym3x3TensorComponent.TENSOR11 +stress_field = stress_container[0] +elemental_values = get_ply_wise_data( + field=stress_field, + ply_name="P3L1__ModelingPly.1", + mesh=composite_model.get_mesh(), + component=component_s11, + spot_reduction_strategy=SpotReductionStrategy.MAX, + requested_location=dpf.locations.elemental, +) +composite_model.get_mesh().plot(elemental_values) + +# %% +# Custom failure criterion +# ~~~~~~~~~~~~~~~~~~~~~~~~ +# The following code block shows how to implement a custom failure criterion. +# It computes the inverse reserve factor for each element with respect to +# fiber failure. The criterion distinguishes between tension and compression. + +# Prepare dict with the material properties. +property_xt = MaterialProperty.Stress_Limits_Xt +property_xc = MaterialProperty.Stress_Limits_Xc +property_dict = composite_model.get_constant_property_dict([property_xt, property_xc]) + +result_field = dpf.field.Field(location=dpf.locations.elemental, nature=dpf.natures.scalar) +with result_field.as_local_field() as local_result_field: + # Process only the layered elements + for element_id in composite_model.get_all_layered_element_ids(): + element_info = composite_model.get_element_info(element_id) + element_irf_max = 0.0 + stress_data = stress_field.get_entity_data_by_id(element_id) + for layer_index, dpf_material_id in enumerate(element_info.dpf_material_ids): + xt = property_dict[dpf_material_id][property_xt] + xc = property_dict[dpf_material_id][property_xc] + selected_indices = get_selected_indices(element_info, layers=[layer_index]) + # Maximum of fiber failure in tension and compression + layer_stress_values = stress_data[selected_indices][:, component_s11] + max_s11 = max(layer_stress_values) + min_s11 = min(layer_stress_values) + if xt > 0 and max_s11 > 0: + element_irf_max = max(max_s11 / xt, element_irf_max) + if xc < 0 and min_s11 < 0: + element_irf_max = max(min_s11 / xc, element_irf_max) + + local_result_field.append([element_irf_max], element_id) + +composite_model.get_mesh().plot(result_field) + +# %% +# Plot deformations on the expanded model +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# You can expand the deformations of the cyclic symmetry model as shown below. +# The same expansion is possible for strains and stresses. For more information, see `Ansys DPF`_. +# +# .. _Ansys DPF: https://dpf.docs.pyansys.com/version/stable/ + +# Get the displacements and expand them +symmetry_option = 2 # fully expand the model +u_cyc = composite_model.core_model.results.displacement() +u_cyc.inputs.read_cyclic(symmetry_option) +# expand the displacements +deformations = u_cyc.outputs.fields_container()[0] + +# Get and expand the mesh +mesh_provider = composite_model.core_model.metadata.mesh_provider +mesh_provider.inputs.read_cyclic(symmetry_option) +mesh = mesh_provider.outputs.mesh() +# Plot the expanded deformations +mesh.plot(deformations) diff --git a/src/ansys/dpf/composites/_composite_model_impl.py b/src/ansys/dpf/composites/_composite_model_impl.py index 213fafbb8..e9beb963b 100644 --- a/src/ansys/dpf/composites/_composite_model_impl.py +++ b/src/ansys/dpf/composites/_composite_model_impl.py @@ -126,7 +126,7 @@ def __init__( data_sources=self.data_sources, material_operators=self.material_operators, unit_system=self._unit_system, - rst_stream_provider=self._get_rst_streams_provider(), + rst_stream_provider=self.get_rst_streams_provider(), ) # self._layup_provider.outputs.layup_model_context_type.get_data() does not work because @@ -154,7 +154,7 @@ def __init__( self._element_info_provider = get_element_info_provider( mesh=self.get_mesh(), - stream_provider_or_data_source=self._get_rst_streams_provider(), + stream_provider_or_data_source=self.get_rst_streams_provider(), material_provider=self.material_operators.material_provider, ) self._layup_properties_provider = LayupPropertiesProvider( @@ -370,7 +370,7 @@ def evaluate_failure_criteria( chunking_data_tree.add({"named_selections": ns_in}) chunking_generator = dpf.Operator("composite::scope_generator") - chunking_generator.inputs.stream_provider(self._get_rst_streams_provider()) + chunking_generator.inputs.stream_provider(self.get_rst_streams_provider()) chunking_generator.inputs.data_tree(chunking_data_tree) if self.data_sources.composite: chunking_generator.inputs.data_sources(self.data_sources.composite) @@ -412,7 +412,7 @@ def evaluate_failure_criteria( self.material_operators.material_provider.outputs ) evaluate_failure_criterion_per_scope_op.inputs.stream_provider( - self._get_rst_streams_provider() + self.get_rst_streams_provider() ) evaluate_failure_criterion_per_scope_op.inputs.mesh(self.get_mesh()) if version_equal_or_later(self._server, "8.0"): @@ -555,7 +555,7 @@ def get_sampling_point( self._material_operators, self.get_mesh(), self._layup_provider, - self._get_rst_streams_provider(), + self.get_rst_streams_provider(), self._data_sources.rst, self._unit_system, time_in, @@ -688,7 +688,7 @@ def get_constant_property_dict( return get_constant_property_dict( material_properties=material_properties, materials_provider=self.material_operators.material_provider, - data_source_or_streams_provider=self._get_rst_streams_provider(), + data_source_or_streams_provider=self.get_rst_streams_provider(), mesh=self.get_mesh(), ) @@ -766,7 +766,8 @@ def get_all_layered_element_ids_for_composite_definition_label( ) return self.get_all_layered_element_ids() - def _get_rst_streams_provider(self) -> Operator: + def get_rst_streams_provider(self) -> Operator: + """Get the streams provider of the loaded result file.""" return self._core_model.metadata.streams_provider def _first_composite_definition_label_if_only_one(self) -> str: diff --git a/src/ansys/dpf/composites/_composite_model_impl_2023r2.py b/src/ansys/dpf/composites/_composite_model_impl_2023r2.py index 0053a8cb7..73b89beea 100644 --- a/src/ansys/dpf/composites/_composite_model_impl_2023r2.py +++ b/src/ansys/dpf/composites/_composite_model_impl_2023r2.py @@ -644,3 +644,7 @@ def _first_composite_definition_label_if_only_one(self) -> str: f"Multiple composite definition keys exist: {self.composite_definition_labels}. " f"Specify a key explicitly." ) + + def get_rst_streams_provider(self) -> Operator: + """Get the streams provider of the loaded result file.""" + return self._core_model.metadata.streams_provider diff --git a/src/ansys/dpf/composites/composite_model.py b/src/ansys/dpf/composites/composite_model.py index fb25d72c4..e02151944 100644 --- a/src/ansys/dpf/composites/composite_model.py +++ b/src/ansys/dpf/composites/composite_model.py @@ -166,6 +166,10 @@ def get_mesh(self, composite_definition_label: Optional[str] = None) -> MeshedRe """ return self._implementation.get_mesh(composite_definition_label) + def get_rst_streams_provider(self) -> Operator: + """Get the streams provider of the loaded result file.""" + return self._implementation.get_rst_streams_provider() + def get_layup_operator(self, composite_definition_label: Optional[str] = None) -> Operator: """Get the lay-up operator. diff --git a/src/ansys/dpf/composites/example_helper/__init__.py b/src/ansys/dpf/composites/example_helper/__init__.py index 8d85bb3a5..48e1f2f80 100644 --- a/src/ansys/dpf/composites/example_helper/__init__.py +++ b/src/ansys/dpf/composites/example_helper/__init__.py @@ -41,7 +41,7 @@ # Example URL to run the examples locally -# EXAMPLE_REPO = "file:////D:/Development/pyansys-example-data/pydpf-composites/" +# EXAMPLE_REPO = "file:////D:/dev/pyansys-example-data/pydpf-composites/" @dataclass @@ -165,6 +165,16 @@ class _ShortFiberExampleLocation: }, ), ), + "cyclic_symmetry": _ContinuousFiberExampleLocation( + directory="cyclic_symmetry", + files=_ContinuousFiberCompositesExampleFilenames( + rst=["file.rst"], + engineering_data="MatML.xml", + composite={ + "solid": _ContinuousFiberCompositeFiles(definition="ACPSolidModel_SM.h5"), + }, + ), + ), } _short_fiber_examples: dict[str, _ShortFiberExampleLocation] = {