From e0508f1d4b51ba1f99718a73af819c8bf9367ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Roos?= <105842014+roosre@users.noreply.github.com> Date: Tue, 4 Jun 2024 12:20:50 +0200 Subject: [PATCH] Provide access to the ply type parameter of the materials (#465) * add property material_metadata to the composite model to provide access to additional material properties such as ply type and solver material index. --------- Co-authored-by: Dominik Gresch --- doc/source/api/layup_info.rst | 1 + .../dpf/composites/_composite_model_impl.py | 153 +++++++----------- .../_composite_model_impl_2023r2.py | 15 +- .../_composite_model_impl_helpers.py | 110 +++++++++++++ src/ansys/dpf/composites/composite_model.py | 21 ++- .../layup_info/material_operators.py | 21 +++ .../layup_info/material_properties.py | 31 +++- .../composites/server_helpers/_versions.py | 5 + tests/composite_model_test.py | 43 ++++- 9 files changed, 303 insertions(+), 97 deletions(-) create mode 100644 src/ansys/dpf/composites/_composite_model_impl_helpers.py diff --git a/doc/source/api/layup_info.rst b/doc/source/api/layup_info.rst index 3d613ee71..b6d33cc5e 100644 --- a/doc/source/api/layup_info.rst +++ b/doc/source/api/layup_info.rst @@ -46,6 +46,7 @@ example shows how to evaluate material properties. get_constant_property_dict :template: autosummary/no_methods_doc/base.rst.jinja2 MaterialProperty + MaterialMetadata diff --git a/src/ansys/dpf/composites/_composite_model_impl.py b/src/ansys/dpf/composites/_composite_model_impl.py index b8586fc79..213fafbb8 100644 --- a/src/ansys/dpf/composites/_composite_model_impl.py +++ b/src/ansys/dpf/composites/_composite_model_impl.py @@ -23,7 +23,7 @@ """Composite Model Interface.""" # New interface after 2023 R2 from collections.abc import Collection, Sequence -from typing import Any, Callable, Optional, cast +from typing import Optional, cast from warnings import warn import ansys.dpf.core as dpf @@ -32,8 +32,9 @@ import numpy as np from numpy.typing import NDArray +from ._composite_model_impl_helpers import _deprecated_composite_definition_label, _merge_containers from .composite_scope import CompositeScope -from .constants import FAILURE_LABEL, REF_SURFACE_NAME, TIME_LABEL, FailureOutput +from .constants import REF_SURFACE_NAME from .data_sources import ( CompositeDataSources, ContinuousFiberCompositesFiles, @@ -54,7 +55,11 @@ _get_reference_surface_and_mapping_field, ) from .layup_info.material_operators import MaterialOperators, get_material_operators -from .layup_info.material_properties import MaterialProperty, get_constant_property_dict +from .layup_info.material_properties import ( + MaterialMetadata, + MaterialProperty, + get_constant_property_dict, +) from .result_definition import FailureMeasureEnum from .sampling_point import SamplingPointNew from .server_helpers import ( @@ -65,84 +70,6 @@ from .unit_system import get_unit_system -def _deprecated_composite_definition_label(func: Callable[..., Any]) -> Any: - """Emit a warning when the deprecated ``composite_definition_label`` is used.""" - function_arg = "composite_definition_label" - - def inner(*args: Sequence[Any], **kwargs: Sequence[Any]) -> Any: - if function_arg in kwargs.keys(): - if kwargs[function_arg]: - warn( - f"Use of {function_arg} is deprecated. Function {func.__name__}." - " can be called without this argument.", - category=DeprecationWarning, - stacklevel=2, - ) - return func(*args, **kwargs) - - return inner - - -def _merge_containers( - non_ref_surface_container: FieldsContainer, ref_surface_container: FieldsContainer -) -> FieldsContainer: - """ - Merge the results fields container. - - Merges the results fields container of the non-reference surface and the reference surface. - """ - assert sorted(non_ref_surface_container.labels) == sorted(ref_surface_container.labels) - - ref_surface_time_ids = ref_surface_container.get_available_ids_for_label(TIME_LABEL) - non_ref_surface_time_ids = non_ref_surface_container.get_available_ids_for_label(TIME_LABEL) - - assert sorted(ref_surface_time_ids) == sorted(non_ref_surface_time_ids) - - out_container = dpf.FieldsContainer() - out_container.labels = [TIME_LABEL, FAILURE_LABEL] - out_container.time_freq_support = non_ref_surface_container.time_freq_support - - def add_to_output_container(time_id: int, source_container: FieldsContainer) -> None: - fields = source_container.get_fields({TIME_LABEL: time_id}) - for field in fields: - failure_enum = _get_failure_enum_from_name(field.name) - out_container.add_field({TIME_LABEL: time_id, FAILURE_LABEL: failure_enum}, field) - - for current_time_id in ref_surface_time_ids: - add_to_output_container(current_time_id, ref_surface_container) - add_to_output_container(current_time_id, non_ref_surface_container) - - return out_container - - -def _get_failure_enum_from_name(name: str) -> FailureOutput: - if name.startswith("Failure Mode"): - if name.endswith(REF_SURFACE_NAME): - return FailureOutput.FAILURE_MODE_REF_SURFACE - else: - return FailureOutput.FAILURE_MODE - - if name.startswith("IRF") or name.startswith("SF") or name.startswith("SM"): - if name.endswith(REF_SURFACE_NAME): - return FailureOutput.FAILURE_VALUE_REF_SURFACE - else: - return FailureOutput.FAILURE_VALUE - - if name.startswith("Layer Index"): - return FailureOutput.MAX_LAYER_INDEX - - if name.startswith("Global Layer in Stack"): - return FailureOutput.MAX_GLOBAL_LAYER_IN_STACK - - if name.startswith("Local Layer in Element"): - return FailureOutput.MAX_LOCAL_LAYER_IN_ELEMENT - - if name.startswith("Solid Element Id"): - return FailureOutput.MAX_SOLID_ELEMENT_ID - - raise RuntimeError("Could not determine failure output from name: " + name) - - class CompositeModelImpl: """Provides access to the basic composite postprocessing functionality. @@ -269,19 +196,15 @@ def material_names(self) -> dict[str, int]: Material name to DPF material ID map. This property can be used to filter analysis plies - or element layers. + or element layers by material name. """ - try: - helper_op = dpf.Operator("composite::materials_container_helper") - except Exception as exc: + helper_op = self._material_operators.material_container_helper_op + if helper_op is None: raise RuntimeError( - f"Operator composite::materials_container_helper doesn't exist. " - f"This could be because the server version is 2024 R1-pre0. The " - f"latest preview or the unified installer can be used instead. " - f"Error: {exc}" - ) from exc + "The used DPF server does not support the requested data. " + "Use version 2024 R1-pre0 or later." + ) - helper_op.inputs.materials_container(self._material_operators.material_provider.outputs) string_field = helper_op.outputs.material_names() material_ids = string_field.scoping.ids @@ -291,6 +214,54 @@ def material_names(self) -> dict[str, int]: return names + @property + def material_metadata(self) -> dict[int, MaterialMetadata]: + """ + DPF material ID to metadata map of the materials. + + This data can be used to filter analysis plies + or element layers by ply type, material name etc. + + Note: ply type is only available in DPF server version 9.0 (2025 R1 pre0) and later. + """ + helper_op = self._material_operators.material_container_helper_op + if helper_op is None: + raise RuntimeError( + "The used DPF server does not support the requested data. " + "Use version 2024 R1-pre0 or later." + ) + material_name_field = helper_op.outputs.material_names() + if hasattr(helper_op.outputs, "solver_material_ids"): + solver_id_field = helper_op.outputs.solver_material_ids() + else: + solver_id_field = None + material_ids = material_name_field.scoping.ids + if hasattr(helper_op.outputs, "ply_types"): + ply_type_field = helper_op.outputs.ply_types() + else: + ply_type_field = None + + metadata = {} + for dpf_mat_id in material_ids: + metadata[dpf_mat_id] = MaterialMetadata( + dpf_material_id=dpf_mat_id, + material_name=material_name_field.data[ + material_name_field.scoping.index(dpf_mat_id) + ], + ply_type=( + ply_type_field.data[ply_type_field.scoping.index(dpf_mat_id)] + if ply_type_field + else None + ), + solver_material_id=( + solver_id_field.data[solver_id_field.scoping.index(dpf_mat_id)] + if solver_id_field + else None + ), + ) + + return metadata + @_deprecated_composite_definition_label def get_mesh(self, composite_definition_label: Optional[str] = None) -> MeshedRegion: """Get the underlying DPF meshed region. diff --git a/src/ansys/dpf/composites/_composite_model_impl_2023r2.py b/src/ansys/dpf/composites/_composite_model_impl_2023r2.py index 66d93e49c..0053a8cb7 100644 --- a/src/ansys/dpf/composites/_composite_model_impl_2023r2.py +++ b/src/ansys/dpf/composites/_composite_model_impl_2023r2.py @@ -47,7 +47,11 @@ get_element_info_provider, ) from .layup_info.material_operators import MaterialOperators, get_material_operators -from .layup_info.material_properties import MaterialProperty, get_constant_property_dict +from .layup_info.material_properties import ( + MaterialMetadata, + MaterialProperty, + get_constant_property_dict, +) from .result_definition import FailureMeasureEnum, ResultDefinition, ResultDefinitionScope from .sampling_point_2023r2 import SamplingPoint2023R2 from .sampling_point_types import SamplingPoint @@ -226,6 +230,15 @@ def material_names(self) -> dict[str, int]: " or later should be used instead." ) + @property + def material_metadata(self) -> dict[int, MaterialMetadata]: + """DPF Material ID to metadata map. Metadata are for example name and ply type.""" + raise NotImplementedError( + "material_metadata is not implemented" + " for this version of DPF. DPF server 9.0 (2025 R1 pre0)" + " or later should be used instead." + ) + def get_layup_operator(self, composite_definition_label: Optional[str] = None) -> Operator: """Get the lay-up operator. diff --git a/src/ansys/dpf/composites/_composite_model_impl_helpers.py b/src/ansys/dpf/composites/_composite_model_impl_helpers.py new file mode 100644 index 000000000..76d536543 --- /dev/null +++ b/src/ansys/dpf/composites/_composite_model_impl_helpers.py @@ -0,0 +1,110 @@ +# 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. + +"""Composite Model Interface.""" +# New interface after 2023 R2 +from collections.abc import Sequence +from typing import Any, Callable +from warnings import warn + +import ansys.dpf.core as dpf +from ansys.dpf.core import FieldsContainer + +from .constants import FAILURE_LABEL, REF_SURFACE_NAME, TIME_LABEL, FailureOutput + + +def _deprecated_composite_definition_label(func: Callable[..., Any]) -> Any: + """Emit a warning when the deprecated ``composite_definition_label`` is used.""" + function_arg = "composite_definition_label" + + def inner(*args: Sequence[Any], **kwargs: Sequence[Any]) -> Any: + if function_arg in kwargs.keys(): + if kwargs[function_arg]: + warn( + f"Use of {function_arg} is deprecated. Function {func.__name__}." + " can be called without this argument.", + category=DeprecationWarning, + stacklevel=2, + ) + return func(*args, **kwargs) + + return inner + + +def _merge_containers( + non_ref_surface_container: FieldsContainer, ref_surface_container: FieldsContainer +) -> FieldsContainer: + """ + Merge the results fields container. + + Merges the results fields container of the non-reference surface and the reference surface. + """ + assert sorted(non_ref_surface_container.labels) == sorted(ref_surface_container.labels) + + ref_surface_time_ids = ref_surface_container.get_available_ids_for_label(TIME_LABEL) + non_ref_surface_time_ids = non_ref_surface_container.get_available_ids_for_label(TIME_LABEL) + + assert sorted(ref_surface_time_ids) == sorted(non_ref_surface_time_ids) + + out_container = dpf.FieldsContainer() + out_container.labels = [TIME_LABEL, FAILURE_LABEL] + out_container.time_freq_support = non_ref_surface_container.time_freq_support + + def add_to_output_container(time_id: int, source_container: FieldsContainer) -> None: + fields = source_container.get_fields({TIME_LABEL: time_id}) + for field in fields: + failure_enum = _get_failure_enum_from_name(field.name) + out_container.add_field({TIME_LABEL: time_id, FAILURE_LABEL: failure_enum}, field) + + for current_time_id in ref_surface_time_ids: + add_to_output_container(current_time_id, ref_surface_container) + add_to_output_container(current_time_id, non_ref_surface_container) + + return out_container + + +def _get_failure_enum_from_name(name: str) -> FailureOutput: + if name.startswith("Failure Mode"): + if name.endswith(REF_SURFACE_NAME): + return FailureOutput.FAILURE_MODE_REF_SURFACE + else: + return FailureOutput.FAILURE_MODE + + if name.startswith("IRF") or name.startswith("SF") or name.startswith("SM"): + if name.endswith(REF_SURFACE_NAME): + return FailureOutput.FAILURE_VALUE_REF_SURFACE + else: + return FailureOutput.FAILURE_VALUE + + if name.startswith("Layer Index"): + return FailureOutput.MAX_LAYER_INDEX + + if name.startswith("Global Layer in Stack"): + return FailureOutput.MAX_GLOBAL_LAYER_IN_STACK + + if name.startswith("Local Layer in Element"): + return FailureOutput.MAX_LOCAL_LAYER_IN_ELEMENT + + if name.startswith("Solid Element Id"): + return FailureOutput.MAX_SOLID_ELEMENT_ID + + raise RuntimeError("Could not determine failure output from name: " + name) diff --git a/src/ansys/dpf/composites/composite_model.py b/src/ansys/dpf/composites/composite_model.py index bca222f77..fb25d72c4 100644 --- a/src/ansys/dpf/composites/composite_model.py +++ b/src/ansys/dpf/composites/composite_model.py @@ -36,7 +36,7 @@ from .failure_criteria import CombinedFailureCriterion from .layup_info import ElementInfo, LayerProperty, LayupModelContextType from .layup_info.material_operators import MaterialOperators -from .layup_info.material_properties import MaterialProperty +from .layup_info.material_properties import MaterialMetadata, MaterialProperty from .result_definition import FailureMeasureEnum from .sampling_point_types import SamplingPoint @@ -131,9 +131,26 @@ def material_operators(self) -> MaterialOperators: @property def material_names(self) -> dict[str, int]: - """Get material name to DPF material ID map.""" + """ + Material name to DPF material ID map. + + This property can be used to filter analysis plies + or element layers by material name. + """ return self._implementation.material_names + @property + def material_metadata(self) -> dict[int, MaterialMetadata]: + """ + DPF material ID to metadata map of the materials. + + This data can be used to filter analysis plies + or element layers by ply type, material name etc. + + Note: ply type is only available in DPF server version 9.0 (2025 R1 pre0) and later. + """ + return self._implementation.material_metadata + def get_mesh(self, composite_definition_label: Optional[str] = None) -> MeshedRegion: """Get the underlying DPF meshed region. diff --git a/src/ansys/dpf/composites/layup_info/material_operators.py b/src/ansys/dpf/composites/layup_info/material_operators.py index 1e0e4ce63..d1dbf45eb 100644 --- a/src/ansys/dpf/composites/layup_info/material_operators.py +++ b/src/ansys/dpf/composites/layup_info/material_operators.py @@ -26,6 +26,8 @@ from ansys.dpf.core import DataSources, Operator +from ansys.dpf.composites.server_helpers import version_equal_or_later + __all__ = ("MaterialOperators", "get_material_operators") from ansys.dpf.composites.unit_system import UnitSystemProvider @@ -61,6 +63,14 @@ def __init__( self._material_support_provider = material_support_provider self._result_info_provider = result_info_provider + if version_equal_or_later(self._material_provider._server, "7.1"): + self._material_container_helper_op = Operator("composite::materials_container_helper") + self._material_container_helper_op.inputs.materials_container( + self._material_provider.outputs + ) + else: + self._material_container_helper_op = None + @property def result_info_provider(self) -> Operator: """Get result_info_provider.""" @@ -83,6 +93,17 @@ def material_provider(self) -> Operator: """Get material_provider.""" return self._material_provider + @property + def material_container_helper_op(self) -> Operator: + """ + Get material container helper operator. + + This operator can be used to access metadata of the materials. + Return value is None if the server version does not support this operator. + The minimum version is 2024 R1-pre0 (7.1). + """ + return self._material_container_helper_op + def get_material_operators( rst_data_source: DataSources, diff --git a/src/ansys/dpf/composites/layup_info/material_properties.py b/src/ansys/dpf/composites/layup_info/material_properties.py index d05b5d772..fcfdacd39 100644 --- a/src/ansys/dpf/composites/layup_info/material_properties.py +++ b/src/ansys/dpf/composites/layup_info/material_properties.py @@ -22,13 +22,15 @@ """Helpers to get material properties.""" from collections.abc import Collection +from dataclasses import dataclass from enum import Enum -from typing import Union, cast +from typing import Optional, Union, cast from ansys.dpf.core import DataSources, MeshedRegion, Operator, types import numpy as np __all__ = ( + "MaterialMetadata", "MaterialProperty", "get_constant_property", "get_all_dpf_material_ids", @@ -209,3 +211,30 @@ def get_constant_property_dict( ) properties[dpf_material_id][material_property] = constant_property return properties + + +@dataclass(frozen=True) +class MaterialMetadata: + """ + Material metadata such as name and ply type. + + Parameters + ---------- + dpf_material_id: + Material index in the DPF materials container. + material_name: + Name of the material. Is empty if the name is not available. + ply_type: + Ply type. One of regular, woven, honeycomb_core, + isotropic_homogeneous_core, orthotropic_homogeneous_core, + isotropic, adhesive, undefined. Regular stands for uni-directional. + None if the DPF server older than 2025 R1 pre 0 (9.0). + solver_material_id: + Material index of the solver. + None if DPF server older than 2024 R1 pre 0 (8.0). + """ + + dpf_material_id: int = 0 + material_name: str = "" + ply_type: Optional[str] = None + solver_material_id: Optional[int] = None diff --git a/src/ansys/dpf/composites/server_helpers/_versions.py b/src/ansys/dpf/composites/server_helpers/_versions.py index add33250d..2350535b1 100644 --- a/src/ansys/dpf/composites/server_helpers/_versions.py +++ b/src/ansys/dpf/composites/server_helpers/_versions.py @@ -50,6 +50,11 @@ class _DpfVersionInfo: "2024 R2 pre 2", "DPF Composites: Failure measure conversion preserves Reference Surface suffix", ), + "9.0": _DpfVersionInfo( + "9.0", + "2025 R1 pre 0", + "DPF Composites: exposure of ply type.", + ), } diff --git a/tests/composite_model_test.py b/tests/composite_model_test.py index 4d1a4c83b..fe759f21d 100644 --- a/tests/composite_model_test.py +++ b/tests/composite_model_test.py @@ -42,7 +42,7 @@ LayupModelContextType, get_analysis_ply_index_to_name_map, ) -from ansys.dpf.composites.layup_info.material_properties import MaterialProperty +from ansys.dpf.composites.layup_info.material_properties import MaterialMetadata, MaterialProperty from ansys.dpf.composites.result_definition import FailureMeasureEnum from ansys.dpf.composites.server_helpers import version_equal_or_later, version_older_than @@ -127,8 +127,47 @@ def test_basic_functionality_of_composite_model(dpf_server, data_files, distribu for mat_name in ref_material_names: assert mat_name in mat_names.keys() - timer.add("After getting properties") + metadata = composite_model.material_metadata + + solver_mat_ids = 4 * [None] + ply_types = 4 * [None] + if version_equal_or_later(dpf_server, "8.0"): + solver_mat_ids = [5, 4, 3, 2] + if version_equal_or_later(dpf_server, "9.0"): + ply_types = ["honeycomb_core", "regular", "woven", "undefined"] + + ref_metadata = { + 1: MaterialMetadata( + dpf_material_id=1, + material_name="Honeycomb", + ply_type=ply_types[0], + solver_material_id=solver_mat_ids[0], + ), + 2: MaterialMetadata( + dpf_material_id=2, + material_name="Epoxy Carbon UD (230 GPa) Prepreg", + ply_type=ply_types[1], + solver_material_id=solver_mat_ids[1], + ), + 3: MaterialMetadata( + dpf_material_id=3, + material_name="Epoxy Carbon Woven (230 GPa) Wet", + ply_type=ply_types[2], + solver_material_id=solver_mat_ids[2], + ), + 4: MaterialMetadata( + dpf_material_id=4, + material_name="Structural Steel", + ply_type=ply_types[3], + solver_material_id=solver_mat_ids[3], + ), + } + + assert len(metadata) == len(ref_metadata) + for dpf_material_id, ref_data in ref_metadata.items(): + assert metadata[dpf_material_id] == ref_data + timer.add("After getting properties") timer.summary()