From 3ae71d1825344efc331fe9490c444b82d4514863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Roos?= <105842014+roosre@users.noreply.github.com> Date: Mon, 9 Sep 2024 21:17:50 +0200 Subject: [PATCH] v0.6.1: improve field indexer for layered solid elements (#515) * Fix Field Indexer and get_element_indices for layered solids and add example of a thermal analysis (#512) * Fix Field Indexer by distinguish between fields with data pointers and without Add draft of an example for a thermal analysis * Bump version 0.6.1 --------- Signed-off-by: dependabot[bot] Co-authored-by: Dominik Gresch Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- doc/source/conf.py | 4 +- examples/013_thermal_example.py | 128 +++++++++ pyproject.toml | 2 +- src/ansys/dpf/composites/_indexer.py | 249 ++++++++++++++---- src/ansys/dpf/composites/constants.py | 6 +- .../dpf/composites/example_helper/__init__.py | 16 +- .../dpf/composites/layup_info/_layup_info.py | 71 ++--- src/ansys/dpf/composites/select_indices.py | 11 +- ...h_all_element_types_all_output_1_layer.rst | Bin 0 -> 393216 bytes ...ment_types_all_output_1_layer_material.xml | 107 ++++++++ ...ment_info_output_all_element_types_test.py | 203 +++++++++++--- tests/performance_test.py | 6 +- tests/test_metadata.py | 2 +- 13 files changed, 652 insertions(+), 153 deletions(-) create mode 100644 examples/013_thermal_example.py create mode 100644 tests/data/all_element_types/model_with_all_element_types_all_output_1_layer.rst create mode 100644 tests/data/all_element_types/model_with_all_element_types_all_output_1_layer_material.xml diff --git a/doc/source/conf.py b/doc/source/conf.py index 6ba5d1f60..312555382 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -3,7 +3,7 @@ from datetime import datetime import os -from ansys_sphinx_theme import ansys_favicon, get_version_match, pyansys_logo_black +from ansys_sphinx_theme import ansys_favicon, get_version_match import numpy as np import pyvista from pyvista.plotting.utilities.sphinx_gallery import DynamicScraper @@ -28,7 +28,6 @@ release = version = __version__ # Select desired logo, theme, and declare the html title -html_logo = pyansys_logo_black html_favicon = ansys_favicon html_theme = "ansys_sphinx_theme" html_short_title = html_title = "PyDPF Composites" @@ -41,6 +40,7 @@ cname = os.environ.get("DOCUMENTATION_CNAME", "composites.dpf.docs.pyansys.com") html_theme_options = { + "logo": "pyansys", "github_url": "https://github.com/ansys/pydpf-composites", "show_prev_next": False, "show_breadcrumbs": True, diff --git a/examples/013_thermal_example.py b/examples/013_thermal_example.py new file mode 100644 index 000000000..53b98b683 --- /dev/null +++ b/examples/013_thermal_example.py @@ -0,0 +1,128 @@ +# 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. + +""" +.. _thermal_example: + +Thermal analysis +---------------- + +PyDPF Composites can also be used to post-process thermal analyses. +In this case, the simulation is a two-step analysis where the results of +a thermal analysis are an input of the structural analysis. So, the RST +contains temperature and structural results. +The example mimics a PCB which was modeled with Ansys Composites PrePost (ACP). +where the solid model feature of ACP is used to generate the volume mesh. + +In detail, the example shows how to extract the temperatures for a specific ply, +and a specific material. + +.. note:: + + When using a Workbench project, + use the :func:`.get_composite_files_from_workbench_result_folder` + method to obtain the input files. + +""" + +# %% +# Set up analysis +# ~~~~~~~~~~~~~~~ +# Setting up the analysis consists of loading the required modules, connecting to the +# DPF server, and retrieving the example files. +# +import ansys.dpf.core as dpf +import numpy as np + +from ansys.dpf.composites.composite_model import CompositeModel +from ansys.dpf.composites.constants import TEMPERATURE_COMPONENT +from ansys.dpf.composites.example_helper import get_continuous_fiber_example_files +from ansys.dpf.composites.layup_info import get_all_analysis_ply_names +from ansys.dpf.composites.ply_wise_data import SpotReductionStrategy, get_ply_wise_data +from ansys.dpf.composites.select_indices import get_selected_indices_by_dpf_material_ids +from ansys.dpf.composites.server_helpers import connect_to_or_start_server + +server = connect_to_or_start_server() +composite_files = get_continuous_fiber_example_files(server, "thermal_solid") + +# %% +# Initialize the model +# ~~~~~~~~~~~~~~~~~~~~ +# The composite model is initialized with the composite files and the server. +# It provides access to the mesh, results, lay-up and materials +composite_model = CompositeModel(composite_files, server) + +# %% +# Get Results - Temperatures +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# The temperatures are stored under structural_temperature +temp_op = composite_model.core_model.results.structural_temperature() +temperatures_fc = temp_op.outputs.fields_container() + +# %% +# Ply-wise results +# ~~~~~~~~~~~~~~~~ +# Ply-wise results can be easily extracted using the function +# :func:`.get_ply_wise_data` and by passing the ply name. + +all_ply_names = get_all_analysis_ply_names(composite_model.get_mesh()) +print(all_ply_names) + +nodal_values = get_ply_wise_data( + field=temperatures_fc, + ply_name="P1L1__ModelingPly.8", + mesh=composite_model.get_mesh(), + component=TEMPERATURE_COMPONENT, + spot_reduction_strategy=SpotReductionStrategy.MAX, + requested_location=dpf.locations.nodal, +) + +composite_model.get_mesh().plot(nodal_values) + +# %% +# Material-wise results +# ~~~~~~~~~~~~~~~~~~~~~ +# It is also possible to filter the results by material. +# In this example the element-wise maximum temperature +# is extracted for the material `Honeycomb Aluminum Alloy`. +print(composite_model.material_names) +material_id = composite_model.material_names["Honeycomb Aluminum Alloy"] + +# get the last result field +temperatures_field = temperatures_fc[-1] + +material_result_field = dpf.field.Field(location=dpf.locations.elemental, nature=dpf.natures.scalar) +# performance optimization: use a local field instead of a field which is pushed to the server +with material_result_field.as_local_field() as local_result_field: + element_ids = temperatures_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 material_id in element_info.dpf_material_ids: + temp_data = temperatures_field.get_entity_data_by_id(element_id) + selected_indices = get_selected_indices_by_dpf_material_ids(element_info, [material_id]) + + value = np.max(temp_data[selected_indices]) + local_result_field.append([value], element_id) + +composite_model.get_mesh().plot(material_result_field) diff --git a/pyproject.toml b/pyproject.toml index 6ca55c195..1231a4489 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "poetry.core.masonry.api" # Check https://python-poetry.org/docs/pyproject/ for all available sections name = "ansys-dpf-composites" # Switch to released version of dpf core releasing pydpf-composites! -version = "0.6.0" +version = "0.6.1" description = "Post-processing of composite structures based on Ansys DPF" license = "MIT" authors = ["ANSYS, Inc. "] diff --git a/src/ansys/dpf/composites/_indexer.py b/src/ansys/dpf/composites/_indexer.py index 193f9e111..6cedb4bbb 100644 --- a/src/ansys/dpf/composites/_indexer.py +++ b/src/ansys/dpf/composites/_indexer.py @@ -22,7 +22,7 @@ """Indexer helper classes.""" from dataclasses import dataclass -from typing import Optional, Protocol, cast +from typing import Optional, Protocol, Union, cast from ansys.dpf.core import Field, PropertyField, Scoping import numpy as np @@ -52,18 +52,56 @@ def setup_index_by_id(scoping: Scoping) -> IndexToId: return IndexToId(mapping=indices, max_id=len(indices) - 1) -class PropertyFieldIndexerSingleValue(Protocol): +class PropertyFieldIndexerProtocol(Protocol): """Protocol for single value property field indexer.""" def by_id(self, entity_id: int) -> Optional[np.int64]: - """Get index by id.""" + """ + Get index by id. + + Note: An exception is thrown if the entry has multiple values. + """ + + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.int64]]: + """Get indices by id.""" + + +def _has_data_pointer(field: Union[PropertyField, Field]) -> bool: + if ( + field._data_pointer is not None # pylint: disable=protected-access + and field._data_pointer.any() # pylint: disable=protected-access + ): + return True + return False + + +def get_property_field_indexer( + field: PropertyField, no_bounds_check: bool +) -> PropertyFieldIndexerProtocol: + """Get indexer for a property field. + + Parameters + ---------- + field: property field + no_bounds_check: whether to get the indexer w/o bounds check. More performant but less safe. + """ + if no_bounds_check: + if _has_data_pointer(field): + return PropertyFieldIndexerWithDataPointerNoBoundsCheck(field) + return PropertyFieldIndexerNoDataPointerNoBoundsCheck(field) + if _has_data_pointer(field): + return PropertyFieldIndexerWithDataPointer(field) + return PropertyFieldIndexerNoDataPointer(field) -class PropertyFieldIndexerArrayValue(Protocol): - """Protocol for array valued property field indexer.""" +class FieldIndexexProtocol(Protocol): + """Protocol for single value field indexer.""" - def by_id(self, entity_id: int) -> Optional[NDArray[np.int64]]: - """Get index by id.""" + def by_id(self, entity_id: int) -> Optional[np.double]: + """Get value by id.""" + + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.double]]: + """Get values by id.""" # General comment for all Indexer: @@ -81,10 +119,15 @@ class PropertyFieldIndexerNoDataPointer: def __init__(self, field: PropertyField): """Create indexer and get data.""" - index_by_id = setup_index_by_id(field.scoping) - self._indices = index_by_id.mapping - self._max_id = index_by_id.max_id - self._data: NDArray[np.int64] = np.array(field.data, dtype=np.int64) + if field.scoping.size > 0: + index_by_id = setup_index_by_id(field.scoping) + self._indices = index_by_id.mapping + self._max_id = index_by_id.max_id + self._data: NDArray[np.int64] = np.array(field.data, dtype=np.int64) + else: + self._indices = np.array([], dtype=np.int64) + self._data = np.array([], dtype=np.int64) + self._max_id = 0 def by_id(self, entity_id: int) -> Optional[np.int64]: """Get index by id. @@ -101,35 +144,17 @@ def by_id(self, entity_id: int) -> Optional[np.int64]: return None return cast(np.int64, self._data[idx]) - -class FieldIndexerNoDataPointer: - """Indexer for a dpf field with no data pointer.""" - - def __init__(self, field: Field): - """Create indexer and get data.""" - if field.scoping.size > 0: - index_by_id = setup_index_by_id(field.scoping) - self._indices = index_by_id.mapping - self._max_id = index_by_id.max_id - self._data: NDArray[np.double] = np.array(field.data, dtype=np.double) - else: - self._indices = np.array([], dtype=np.int64) - self._max_id = 0 - self._data = np.array([], dtype=np.double) - - def by_id(self, entity_id: int) -> Optional[np.double]: - """Get index by ID. + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.int64]]: + """Get indices by id. Parameters ---------- entity_id """ - if entity_id > self._max_id: - return None - idx = self._indices[entity_id] - if idx < 0: + value = self.by_id(entity_id) + if value is None: return None - return cast(np.double, self._data[idx]) + return np.array([value], dtype=np.int64) class PropertyFieldIndexerNoDataPointerNoBoundsCheck: @@ -159,6 +184,18 @@ def by_id(self, entity_id: int) -> Optional[np.int64]: else: return cast(np.int64, self._data[self._indices[entity_id]]) + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.int64]]: + """Get indices by id. + + Parameters + ---------- + entity_id + """ + value = self.by_id(entity_id) + if value is None: + return None + return np.array([value], dtype=np.int64) + class PropertyFieldIndexerWithDataPointer: """Indexer for a property field with data pointer.""" @@ -183,9 +220,18 @@ def __init__(self, field: PropertyField): self._n_components = 0 self._data_pointer = np.array([], dtype=np.int64) - def by_id(self, entity_id: int) -> Optional[NDArray[np.int64]]: + def by_id(self, entity_id: int) -> Optional[np.int64]: """Get index by ID. + Parameters + ---------- + entity_id + """ + raise NotImplementedError("PropertyFieldIndexerWithDataPointer does not support by_id.") + + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.int64]]: + """Get indices by ID. + Parameters ---------- entity_id @@ -206,44 +252,50 @@ def by_id(self, entity_id: int) -> Optional[NDArray[np.int64]]: ) -class FieldIndexerWithDataPointer: - """Indexer for a dpf field with data pointer.""" +class PropertyFieldIndexerWithDataPointerNoBoundsCheck: + """Indexer for a property field with data pointer and no bounds checks.""" - def __init__(self, field: Field): + def __init__(self, field: PropertyField): """Create indexer and get data.""" if field.scoping.size > 0: index_by_id = setup_index_by_id(field.scoping) self._indices = index_by_id.mapping - self._max_id = index_by_id.max_id - self._data: NDArray[np.double] = np.array(field.data, dtype=np.double) + self._data: NDArray[np.int64] = np.array(field.data, dtype=np.int64) self._n_components = field.component_count self._data_pointer: NDArray[np.int64] = np.append( field._data_pointer, len(self._data) * self._n_components ) else: - self._max_id = 0 self._indices = np.array([], dtype=np.int64) - self._data = np.array([], dtype=np.double) + self._data = np.array([], dtype=np.int64) self._n_components = 0 self._data_pointer = np.array([], dtype=np.int64) - def by_id(self, entity_id: int) -> Optional[NDArray[np.double]]: + def by_id(self, entity_id: int) -> Optional[np.int64]: """Get index by ID. Parameters ---------- entity_id """ - if entity_id > self._max_id: - return None + raise NotImplementedError( + "PropertyFieldIndexerWithDataPointerNoBoundsCheck does not support by_id." + ) + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.int64]]: + """Get index by ID. + + Parameters + ---------- + entity_id + """ idx = self._indices[entity_id] if idx < 0: return None return cast( - NDArray[np.double], + NDArray[np.int64], self._data[ self._data_pointer[idx] // self._n_components : self._data_pointer[idx + 1] @@ -252,39 +304,128 @@ def by_id(self, entity_id: int) -> Optional[NDArray[np.double]]: ) -class PropertyFieldIndexerWithDataPointerNoBoundsCheck: - """Indexer for a property field with data pointer and no bounds checks.""" +# DPF does not set the data pointers if a field has just +# one value per entity. Therefore, it is unknown if +# data pointer are set for some field of layered elements, for +# instance angles, thickness etc. +def get_field_indexer(field: Field) -> FieldIndexexProtocol: + """Get field indexer based on data pointer. - def __init__(self, field: PropertyField): + Parameters + ---------- + field + """ + if _has_data_pointer(field): + return FieldIndexerWithDataPointer(field) + return FieldIndexerNoDataPointer(field) + + +class FieldIndexerNoDataPointer: + """Indexer for a dpf field with no data pointer.""" + + def __init__(self, field: Field): """Create indexer and get data.""" if field.scoping.size > 0: index_by_id = setup_index_by_id(field.scoping) self._indices = index_by_id.mapping + self._max_id = index_by_id.max_id + self._data: NDArray[np.double] = np.array(field.data, dtype=np.double) + else: + self._indices = np.array([], dtype=np.int64) + self._max_id = 0 + self._data = np.array([], dtype=np.double) - self._data: NDArray[np.int64] = np.array(field.data, dtype=np.int64) + def by_id(self, entity_id: int) -> Optional[np.double]: + """Get value by ID. + + Parameters + ---------- + entity_id + """ + if entity_id > self._max_id: + return None + idx = self._indices[entity_id] + if idx < 0: + return None + return cast(np.double, self._data[idx]) + + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.double]]: + """Get values by id. + + Parameters + ---------- + entity_id + """ + value = self.by_id(entity_id) + if value is None: + return None + return np.array([value], dtype=np.double) + + +class FieldIndexerWithDataPointer: + """Indexer for a dpf field with data pointer.""" + + def __init__(self, field: Field): + """Create indexer and get data.""" + if field.scoping.size > 0: + index_by_id = setup_index_by_id(field.scoping) + self._indices = index_by_id.mapping + self._max_id = index_by_id.max_id + + self._data: NDArray[np.double] = np.array(field.data, dtype=np.double) self._n_components = field.component_count self._data_pointer: NDArray[np.int64] = np.append( field._data_pointer, len(self._data) * self._n_components ) else: + self._max_id = 0 self._indices = np.array([], dtype=np.int64) - self._data = np.array([], dtype=np.int64) + self._data = np.array([], dtype=np.double) self._n_components = 0 self._data_pointer = np.array([], dtype=np.int64) - def by_id(self, entity_id: int) -> Optional[NDArray[np.int64]]: - """Get index by ID. + def by_id(self, entity_id: int) -> Optional[np.double]: + """Get value by ID. Parameters ---------- entity_id """ + values = self.by_id_as_array(entity_id) + if values is None or len(values) == 0: + return None + if len(values) == 1: + return cast(np.double, values[0]) + + # There is an issue with the DPF server 2024r1_pre0 and before. + # Values of the laminate offset field does not have length 1. + # In this case the format of values is [offset, 0, 0., ...] + offset = values[0] + if all([v == 0 for v in values[1:]]): + return cast(np.double, offset) + + raise RuntimeError( + f"Cannot extract value for entity {entity_id}. " + "Use the latest version of the DPF server to get the correct value. " + f"Values: {values}" + ) + + def by_id_as_array(self, entity_id: int) -> Optional[NDArray[np.double]]: + """Get values by ID. + + Parameters + ---------- + entity_id + """ + if entity_id > self._max_id: + return None + idx = self._indices[entity_id] if idx < 0: return None return cast( - NDArray[np.int64], + NDArray[np.double], self._data[ self._data_pointer[idx] // self._n_components : self._data_pointer[idx + 1] diff --git a/src/ansys/dpf/composites/constants.py b/src/ansys/dpf/composites/constants.py index fbe20c507..4047bb867 100644 --- a/src/ansys/dpf/composites/constants.py +++ b/src/ansys/dpf/composites/constants.py @@ -30,10 +30,13 @@ "REF_SURFACE_NAME", "FAILURE_LABEL", "TIME_LABEL", + "TEMPERATURE_COMPONENT", ) FAILURE_LABEL = "failure_label" TIME_LABEL = "time" +TEMPERATURE_COMPONENT = 0 +REF_SURFACE_NAME = "Reference Surface" class Spot(IntEnum): @@ -69,6 +72,3 @@ class FailureOutput(IntEnum): MAX_GLOBAL_LAYER_IN_STACK = 5 MAX_LOCAL_LAYER_IN_ELEMENT = 6 MAX_SOLID_ELEMENT_ID = 7 - - -REF_SURFACE_NAME = "Reference Surface" diff --git a/src/ansys/dpf/composites/example_helper/__init__.py b/src/ansys/dpf/composites/example_helper/__init__.py index 9dc4c152a..8d85bb3a5 100644 --- a/src/ansys/dpf/composites/example_helper/__init__.py +++ b/src/ansys/dpf/composites/example_helper/__init__.py @@ -40,6 +40,10 @@ EXAMPLE_REPO = "https://github.com/ansys/example-data/raw/master/pydpf-composites/" +# Example URL to run the examples locally +# EXAMPLE_REPO = "file:////D:/Development/pyansys-example-data/pydpf-composites/" + + @dataclass class _ContinuousFiberCompositeFiles: definition: str @@ -62,7 +66,7 @@ class _ShortFiberCompositesExampleFilenames: @dataclass class _ContinuousFiberExampleLocation: - """Location of the a given continuous fiber example in the example_data repo. + """Location of a given continuous fiber example in the example_data repo. Parameters ---------- @@ -151,6 +155,16 @@ class _ShortFiberExampleLocation: }, ), ), + "thermal_solid": _ContinuousFiberExampleLocation( + directory="thermal_solid", + files=_ContinuousFiberCompositesExampleFilenames( + rst=["file.rst"], + engineering_data="MatML.xml", + composite={ + "shell": _ContinuousFiberCompositeFiles(definition="ACPSolidModel_SM.h5"), + }, + ), + ), } _short_fiber_examples: dict[str, _ShortFiberExampleLocation] = { diff --git a/src/ansys/dpf/composites/layup_info/_layup_info.py b/src/ansys/dpf/composites/layup_info/_layup_info.py index 44bf6b5b3..cbe1e215c 100644 --- a/src/ansys/dpf/composites/layup_info/_layup_info.py +++ b/src/ansys/dpf/composites/layup_info/_layup_info.py @@ -34,16 +34,7 @@ import numpy as np from numpy.typing import NDArray -from .._indexer import ( - FieldIndexerNoDataPointer, - FieldIndexerWithDataPointer, - PropertyFieldIndexerArrayValue, - PropertyFieldIndexerNoDataPointer, - PropertyFieldIndexerNoDataPointerNoBoundsCheck, - PropertyFieldIndexerSingleValue, - PropertyFieldIndexerWithDataPointer, - PropertyFieldIndexerWithDataPointerNoBoundsCheck, -) +from .._indexer import get_field_indexer, get_property_field_indexer from ..server_helpers import version_equal_or_later, version_older_than from ._enums import LayupProperty @@ -211,7 +202,7 @@ def __init__(self, mesh: MeshedRegion, name: str): """Initialize AnalysisPlyProvider.""" self.name = name self.property_field = _get_analysis_ply(mesh, name) - self._layer_indices = PropertyFieldIndexerNoDataPointer(self.property_field) + self._layer_indices = get_property_field_indexer(self.property_field, False) def get_layer_index_by_element_id(self, element_id: int) -> Optional[np.int64]: """Get the layer index for the analysis ply in a given element. @@ -404,31 +395,22 @@ def __init__( # focused on the most important properties. We can add different providers # for other properties (such as thickness and angles) - def get_indexer_with_data_pointer(field: PropertyField) -> PropertyFieldIndexerArrayValue: - if no_bounds_checks: - return PropertyFieldIndexerWithDataPointerNoBoundsCheck(field) - else: - return PropertyFieldIndexerWithDataPointer(field) - - def get_indexer_no_data_pointer(field: PropertyField) -> PropertyFieldIndexerSingleValue: - if no_bounds_checks: - return PropertyFieldIndexerNoDataPointerNoBoundsCheck(field) - else: - return PropertyFieldIndexerNoDataPointer(field) - # Has to be always with bounds checks because it does not contain # data for all the elements - self.layer_indices = PropertyFieldIndexerWithDataPointer(layer_indices) - self.layer_materials = get_indexer_with_data_pointer(material_ids) - self.apdl_element_types = get_indexer_no_data_pointer(element_types_apdl) - self.dpf_element_types = get_indexer_no_data_pointer(element_types_dpf) - self.keyopt_8_values = get_indexer_no_data_pointer(keyopt_8_values) - self.keyopt_3_values = get_indexer_no_data_pointer(keyopt_3_values) + self.layer_indices = get_property_field_indexer(layer_indices, no_bounds_checks) + self.layer_materials = get_property_field_indexer(material_ids, no_bounds_checks) + + self.apdl_element_types = get_property_field_indexer(element_types_apdl, no_bounds_checks) + self.dpf_element_types = get_property_field_indexer(element_types_dpf, no_bounds_checks) + self.keyopt_8_values = get_property_field_indexer(keyopt_8_values, no_bounds_checks) + self.keyopt_3_values = get_property_field_indexer(keyopt_3_values, no_bounds_checks) self.mesh = mesh self.corner_nodes_by_element_type = _get_corner_nodes_by_element_type_array() - self.apdl_material_indexer = get_indexer_no_data_pointer(self.mesh.elements.materials_field) + self.apdl_material_indexer = get_property_field_indexer( + self.mesh.elements.materials_field, no_bounds_checks + ) self.solver_material_to_dpf_id = {} if solver_material_ids is not None: @@ -473,9 +455,10 @@ def get_element_info(self, element_id: int) -> Optional[ElementInfo]: if element_type is None: raise IndexError(f"No DPF element type for element with id {element_id}.") - layer_data = self.layer_indices.by_id(element_id) + layer_data = self.layer_indices.by_id_as_array(element_id) if layer_data is not None: - dpf_material_ids = self.layer_materials.by_id(element_id) + # can be of type int for single layer elements or array for multilayer materials + dpf_material_ids = self.layer_materials.by_id_as_array(element_id) assert dpf_material_ids is not None assert layer_data[0] + 1 == len(layer_data), "Invalid size of layer data" n_layers = layer_data[0] @@ -605,24 +588,24 @@ def __init__(self, layup_provider: Operator, mesh: MeshedRegion): layup_outputs_container = layup_provider.outputs.section_data_container() composite_label = layup_outputs_container.labels[0] angle_field = layup_outputs_container.get_field({composite_label: LayupProperty.ANGLE}) - self._angle_indexer = FieldIndexerWithDataPointer(angle_field) + self._angle_indexer = get_field_indexer(angle_field) thickness_field = layup_outputs_container.get_field( {composite_label: LayupProperty.THICKNESS} ) - self._thickness_indexer = FieldIndexerWithDataPointer(thickness_field) + self._thickness_indexer = get_field_indexer(thickness_field) shear_angle_field = layup_outputs_container.get_field( {composite_label: LayupProperty.SHEAR_ANGLE} ) - self._shear_angle_indexer = FieldIndexerWithDataPointer(shear_angle_field) + self._shear_angle_indexer = get_field_indexer(shear_angle_field) offset_field = layup_outputs_container.get_field( {composite_label: LayupProperty.LAMINATE_OFFSET} ) - self._offset_indexer = FieldIndexerNoDataPointer(offset_field) + self._offset_indexer = get_field_indexer(offset_field) self._index_to_name_map = get_analysis_ply_index_to_name_map(mesh) - self._analysis_ply_indexer = PropertyFieldIndexerWithDataPointer( - mesh.property_field("layer_to_analysis_ply") + self._analysis_ply_indexer = get_property_field_indexer( + mesh.property_field("layer_to_analysis_ply"), False ) def get_layer_angles(self, element_id: int) -> Optional[NDArray[np.double]]: @@ -633,7 +616,7 @@ def get_layer_angles(self, element_id: int) -> Optional[NDArray[np.double]]: element_id: Element Id/Label """ - return self._angle_indexer.by_id(element_id) + return self._angle_indexer.by_id_as_array(element_id) def get_layer_thicknesses(self, element_id: int) -> Optional[NDArray[np.double]]: """Get thicknesses for all layers. Returns None if element is not layered. @@ -644,7 +627,7 @@ def get_layer_thicknesses(self, element_id: int) -> Optional[NDArray[np.double]] Element Id/Label """ - return self._thickness_indexer.by_id(element_id) + return self._thickness_indexer.by_id_as_array(element_id) def get_layer_shear_angles(self, element_id: int) -> Optional[NDArray[np.double]]: """Get shear angle for all layers. Returns None if element is not layered. @@ -654,7 +637,7 @@ def get_layer_shear_angles(self, element_id: int) -> Optional[NDArray[np.double] element_id: Element Id/Label """ - return self._shear_angle_indexer.by_id(element_id) + return self._shear_angle_indexer.by_id_as_array(element_id) def get_element_laminate_offset(self, element_id: int) -> Optional[np.double]: """Get laminate offset of element. Returns None if element is not layered. @@ -676,7 +659,7 @@ def get_analysis_plies(self, element_id: int) -> Optional[Sequence[str]]: Element Id/Label """ - indexes = self._analysis_ply_indexer.by_id(element_id) - if indexes is None: + indices = self._analysis_ply_indexer.by_id_as_array(element_id) + if indices is None: return None - return [self._index_to_name_map[index] for index in indexes] + return [self._index_to_name_map[index] for index in indices] diff --git a/src/ansys/dpf/composites/select_indices.py b/src/ansys/dpf/composites/select_indices.py index 2e4c8798e..28ea98148 100644 --- a/src/ansys/dpf/composites/select_indices.py +++ b/src/ansys/dpf/composites/select_indices.py @@ -142,10 +142,17 @@ def get_selected_indices( ) # Todo: Use numpy. Probably use ravel_multi_index method. current_index = 0 + + num_nodes_per_spot = ( + element_info.number_of_nodes_per_spot_plane + if element_info.is_layered + else element_info.n_corner_nodes + ) + for layer_index in layer_indices: - layer_start_index = layer_index * element_info.n_corner_nodes * element_info.n_spots + layer_start_index = layer_index * num_nodes_per_spot * element_info.n_spots for spot_index in spot_indices: - spot_start_index = layer_start_index + spot_index * element_info.n_corner_nodes + spot_start_index = layer_start_index + spot_index * num_nodes_per_spot for corner_index in node_indices: all_indices[current_index] = spot_start_index + corner_index current_index = current_index + 1 diff --git a/tests/data/all_element_types/model_with_all_element_types_all_output_1_layer.rst b/tests/data/all_element_types/model_with_all_element_types_all_output_1_layer.rst new file mode 100644 index 0000000000000000000000000000000000000000..9173fe6026566aa05268ba1cedb7739fd4cdb142 GIT binary patch literal 393216 zcmeFa2VfP&w?Dr0&{2?H0->bQizq2e8li|FRTPK;X#&y(1-;Tin%L+D2#`kaka9^u zq=O(;iHbB)Kxrz-?|gR8+`C+o2=D3pf8YC`fs@afnR8}lKT~#g?%t%gW&~Rif(I13 z7t5^`J>4i5bTiBOvq7Ck4bGfwlq;ld%P#I6y1eM#sLANp2lnsdUax0^Hf=hExpR2) z&0)hx9ge8en>1b1;#=l2lJGMxiSlRHN5C+qACF}Ghb5q$=9Q@yKN%`s;C18Af?99F zc4iSq-&`SmElG)5nwFoiAxTLR1k?-v4@g<_qNW{LtZ5@jPowm_B_D2;wO_4uq8!&? z^(=SK91lJ6=WYc%OF8Jq90S_@cOf?!a6ut|XZxi2;1Qfru;H3eR~{17myZN_5N_uq zHl--<8IsZ@Wk|}Blp`rmQh_9lqyhLQ{;8BQ{SWF*O_B%hJYBbiUKfMg-bA`%+DS|O6cBv_Y2v#WIV|Pl8GdfNG6j^A(=`tjpQ)NS0u+tj*y%rIZbkgi0MOng9mUx$vTo~so*jL(2&t2u={ApFq z_z#S8-OYH_>Lz~0h_%1cv9Ht1$aM-65zn%3Kr{v;}RK^f1Ipv-jgyHA4l?jl4T5X38! zBvTs7Mm&rJK3ge{fVx|epuDz>YbagWcjL6>hP7En4@yJ(KBF|01s!D%Bq>iam;_^O z1PSuSkQ^gYCowf{;`nsr{G4QgIprN+DVAIj)K0=q~OT;Duu zFRp{YxDNDVZffXUiX$K6^(HAV(8XB$G>X#Er|4@n*OrssCN#W5{igNXsJS+Q(onu@ zzrM!xq7LL|HP_Hy)vxGNw2gw%uQg5G=vSSy# ztdkf6XcPJ#{RwpKOI$zF(C@D6V^;l$GE_fiwLadZhG(@tMo>lQv;Xe;Xze>(UsipG zHmbfuzghdPI>iw%S9TC))nBSFeOYhX zo>c$+8|&!f^c~t^?YjtyQ!wV8hp8KVhrNvJI*Re2`shjSaXIZf(D#K%z&jBA0YrZ! z)A+zRLR{U~kl%^~_LxsVwJyRA>mt(Begk?|^9i=-k76X4PpEGM3Dy+sH>Q)kOA=0^ z*1#Cjac_Ipa&PNGX&Ae%YvAMTH_!&v7ZIfQBcWjQMGaFo`XZTh>w1^n{Q1AW_Tk=@ z)!H|eIt_j0y7qOTzN+_J{cv@^3L_iz73|Q@s*kMm2I+sd=Aob7B%xsRRV`CD`U-ph zoc1A?dCfyT7|Uv{gPzrW>fhLp5RJxu1oIj54{?ltAm)qW33Cp7VJ`wZ%r~@K&9|)X zU)K2+Nw&DI?Dioaa{24XNv8^|^Y0*4gg(7aqV|>M{-c3o|1pvBto>>2N2LE(DYC_PMfl9Q^K&xx?L3=HdG9SoZ3}gH%v6M&aBI;$u{ab-E`PxZR_b` zTf(rlUaylAZ??_pT720?ah+4$bsXhsM##ywo{R0H?Zo^xLnGb9M>#6Y7Mj+WZ64+b zYd-~J+!Qx;`q@M`Rb{KtO^3ae*P^B&{d`t8Ri`cNth_qg+mvk|<<;5V=UmD&uRrVd zKJQ|y+E5~g_BM0zG0V4Jo3kweY@2-_J8c8mR?R1~kF##K?qaLj{V30l@@>jTwI!!^ zH+LyN2M%qnwyG_Ua_DG_7Q*GLIsfQ*cG%jKt(yBe@e<1ADqGbSlb0;Hv?(9emdEhX z!nmIDjG4Sn=5)<1*|r*EPS>n#t3PC$W&5>OY@=)+qy0AJRTOX|xUyrl=zxB~weUz`sG3ui5aKG3(>qe@k=C`wr*|yEDeAGJf7(QATu1EQ( zw&k$)=*ma6?J<08%15m|MY!G^IM7~Zn`(@YaXq^7QR~WM_}E;19^-oK%15n5k8?fU z_&PkF;Chsg_v8E6TzwwrdX$eB*Yoi8$nfvm5o|o${PF6 zLYDHJ`>VsJbiW(BAIZw6|go?RE7r%eP*e zgFCGUm#@aKd2LzR>dIF2bq+k+Ty530KFYJBEq3Lj+LBYddvd!~TXNt~cePb*$%(^= zEP2UtOhqVLRjz8kd97Lc*j#d{p@gbK)zK z`BLRShL5|ekMcF^$#VU%E1zoFuRoc}M)h5_9M+#++-_y7uv+%(kGrdnS$>x7)s=1a z?AsfmY*gLVvv04>)z;}#NXurfM7jEy2-D!TYjwD4@kbJ&}^yZSim!SzBY zJbcZ%@=@2Uc*>!k2<4;3pYkd4$T)OdyG{8xZL__{L@8UL3;MJOL<`DRan}}IP1&ydNP%dvrXnD$+?9{BO@CXDu6)XK zeQpjcM9vTj4|5%%eAF|uYFiHDz@~i4=|+iKA;uiIj#54q9k!-#4)xfTPX+dQjCwMa zkK#b_mqR^Te=c9uQ_RuELexkIg@<`_S3Zh^$MDgWkB4rQsJS^X+j=5geIBQO*vD3l z%YO{sqEtQA+2=9pv8#I2^*lyBnXW#M!J9UK+oaYv#aj-~J?_d!UB{!(sE+ktSGKC$ z^4aedBb2S{`kcew-{$J$tS8%kk5WGBn$PAVL@_CGU++BT?Zw_tKm5*wZswW4oBV2uyZw@{-<)hm4 z7(P+T$BVg8ZOdWZx4ZhNdUEi|WFMO+*HefwhkKtkh;6(bwwe)gsK=dsZ0dR*qaNMW z=i#-rrdg@Y%S7Y-RH*y`ix$D@?=#L~oW~f=n8A32@f*g=jJFue=Q5R=j13q&F!p48 zgAwg3V7lxnMp)b*?_g{uG5_VJw@31_O+mJM8g}>dIBW{hTYJxFrRn|Nvh+@TdE?z{ z*j6^}ZKx_Y36QHbbNFW|#xTMbB;h12Nm`M#CTT1{z4=a_TV~;-!D#W*h!L$*y^YC*9K^BXg_L~vVpX8>up7T>K z@*&yvNnyJ3Dw;d}t4i-ud(c1A*2+gteew+1BeWxFPx2B=2f~gdomjRLCXyf@{HBoX zB0;{LWG6`yNiqxSf3(fp4L&MsgXx{FA=*%Fm^Pf=L>kGqkJ=|2_6_LmvCp)5+I($+ zwoqH7EzZ`yTrTjPhnOv_714@nE43IcR$G;=y*c)aXrA<*xwqz{`SL%r?bXPS!W>*n zE<-iP8T6buEzP#w2wx$2m83gK4;M+LIOh8oBxx>ruuUh~O|pmNOA@r_QI1Sbt7xO? zUCOuV-#gk^Z5;hGZ6CEaIc=y#X-lVwj3y*d+c)+FM2*Gtp5pkkagRXkGdY^GvM#8=Mk>U>%SZI zJPy~Ye>}KtkNP}14vOeK4X&?&i)=YC`%pcjt9pv)`#PfEjz7Y+bL=P|=Q!vRn!S(W zO>wQp!JWt8TKQZuuBV#*M-D#b^*FCXU7tDLvgS>-Es@7)-BPIF+~4!2d+28QCwaUT z()Sy@Jvv4myg7WFo9V;%4r5J)uHtU2zi3uiqA8&&Gq;&WKh^V^X3S&EWlAhj zqJ+IUhISr`G-`!dgt*xGRA-NryiNTP&xW@#}$?C?9pG z`1hnEsQSJm-RaNu+QRIny_^0m<)dGf|4Gsv?IFA46wgbUoiW5{T$*@`QO)=3DfP?( z=;!so2K3VyG7l~WHYS~lpl~q|9L_X}wJoF+vou{r^|&u&!LR^yM{+{%>64oqxIff1}NhQ>O7H&&M$ooBtII;9il>Y-tJX z?J;bz&o@Gu8#?l`%2)To$I16IODi#gP~A66m}&G=-M2N}Ombr^!B~p1EMrARcgAXr z)fsCtdNO)5`ZCsGtfw!c`=$8|WvgGd!I#bH8$xA^%{oPBTVHR%ZbGfWf=~AW*Oy+f z2kD`i`-MeHG53o*}_`1fE^Xl9VGUPl9KQFp?G|;Uq0dT9N#B&f%g8Ofs0{O_CubLrI2_3@5>y z#W~fdB%hJYBbiUKfMg-bB9g@?2D6>Ac-SMAc-fz{=6YcBNFV-dy~9Q(ubrk$zBqi zjqD@YPjZmt5XoASk|Y$2aSR?Ye!&&SZEh0qit&toM?HB+z%#}-#sTUDzj;Z(J;prB z1ZNa%_<=a;((a}Fo{K<96e{5G8tJ#muw*_nAwOL#?5rU_MUVzJv;%d*9O?gdaQ_bv z$3k*xMNc=%g|SiLcwOy1-hIKgxhhKRD{X9$4()ehP`Wt^F?!*(#hwVaPbBznc@A0ta|G@c0f8OT3;l)0QIH$Je<6|87t2OeR zOTGu?C0|}P;BJ?peurK14zlmf?iH5|n!G9SfJ^>9&M$uUjZ-T|tV`VMlD~)Z|1|wm zzHrHJMs?q~)ywOy?<&7-E_qwnH?(0=m(Ax_25xrA-^lqdnE5FiT=Lg({%f4S)+K*6 z#j#Hg=JJ1xEw=vsLotE38Q<&pMWa^FtqII^sPrcp2}=SCG7c-*@Qb(Gyq0*E?ang3 zYucwg#dcndMT+XxzFj`g55Hr}+Ap7Q`xYM=e)jz6^?@I|!5vB(dw%Qfbwyzr~uW!w?6PZ{7+Pd|!d`}$XlxX=bcb(3-}YP$LGz0N7K_oi)X zUUp1iOwrvQv#w7L9May5AKu)=$hV)&Kzd52RqGC{96U5I$bG$mgMNLz_^i=oliEy~ zU*t};PXhCNUw2+=!Igop*(Y>)yyY63_ zeqLW2_9-vAv^Ur#-kjp-qtudXT738Rra;{#KY;VUop~g4SD$-+{x10$xo>_QaIHX69k#RAG5uaSYuKmca;XOc9{i52@Y(XA&+Q2O zi{jQfaF6qwOsR6aLPGn*KV9;F=lpS|f685#{6bXt&u_h6uwz8;#LF&u7uokvofDpY zsqPCd`R6%5b@#{ZmffD4_=8LScbuPL`ln>N zeSPTTf@2cnT=Ke7UcOhh931A=z<(j-;Y{Gr*BjT@?KL-W<9QP={XR3S+tnF?HGX>f zH|>WRfga~fEO=~k!)>9r{5E}K+8sJ%;=Olg!9K;Sa7n|yQ5h3!->5NUL#>{ zk>b=0?YmRazpZPyEpUQM{`;Kπ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 literal 0 HcmV?d00001 diff --git a/tests/data/all_element_types/model_with_all_element_types_all_output_1_layer_material.xml b/tests/data/all_element_types/model_with_all_element_types_all_output_1_layer_material.xml new file mode 100644 index 000000000..815ab7248 --- /dev/null +++ b/tests/data/all_element_types/model_with_all_element_types_all_output_1_layer_material.xml @@ -0,0 +1,107 @@ + + + + + + + 1 + + - + ACP + Isotropic + + + - + Isotropic + + 70000 + + + 0.30000000000000004 + + + 26923.076923076922 + + + + + + + 2 + + - + ACP + Isotropic + + + - + Isotropic + + 210000 + + + 0.30000000000000004 + + + 80769.230769230766 + + + + + + + Ply Type + + + + Elasticity + + + + Young's Modulus + + + [Length] + + + [Mass] + + + [Time] + + + + + Poisson's Ratio + + + + Shear Modulus + + + [Length] + + + [Mass] + + + [Time] + + + + + + + + + + 1 + 2d0cf88a-4ac7-4d14-b949-21b9f3a3f932 + + + 2 + af1cc58e-261a-40eb-b06c-95c53f017823 + + + + diff --git a/tests/element_info_output_all_element_types_test.py b/tests/element_info_output_all_element_types_test.py index dc9cdf336..921e36e0b 100644 --- a/tests/element_info_output_all_element_types_test.py +++ b/tests/element_info_output_all_element_types_test.py @@ -25,16 +25,19 @@ import pathlib import ansys.dpf.core as dpf -from ansys.dpf.core import Field, MeshedRegion, PropertyField +from ansys.dpf.core import Field, MeshedRegion, PropertyField, unit_systems +import numpy as np import pytest +from ansys.dpf.composites.composite_model import CompositeModel from ansys.dpf.composites.constants import Spot +from ansys.dpf.composites.data_sources import ContinuousFiberCompositesFiles from ansys.dpf.composites.layup_info import ElementInfo, get_element_info_provider from ansys.dpf.composites.select_indices import ( get_selected_indices, get_selected_indices_by_dpf_material_ids, ) -from ansys.dpf.composites.server_helpers import upload_file_to_unique_tmp_folder +from ansys.dpf.composites.server_helpers import upload_file_to_unique_tmp_folder, version_older_than @dataclass(frozen=True) @@ -47,6 +50,7 @@ class ExpectedOutput: # otherwise it is 2 or 3 for top/bot or top/bot/mid output n_spots: int is_layered: bool + number_of_nodes_per_spot: int @dataclass(frozen=True) @@ -111,25 +115,25 @@ def get_expected_output( n_spots_homogeneous_solid = 0 return { - 1: ExpectedOutput(3, 4, 181, n_spots_shell, True), - 2: ExpectedOutput(3, 3, 181, n_spots_shell, True), - 3: ExpectedOutput(3, 4, 281, n_spots_shell, True), - 4: ExpectedOutput(3, 3, 281, n_spots_shell, True), - 10: ExpectedOutput(1, 8, 185, n_spots_homogeneous_solid, False), - 11: ExpectedOutput(1, 6, 185, n_spots_homogeneous_solid, False), - 12: ExpectedOutput(1, 5, 185, n_spots_homogeneous_solid, False), - 13: ExpectedOutput(1, 4, 185, n_spots_homogeneous_solid, False), - 20: ExpectedOutput(1, 8, 186, n_spots_homogeneous_solid, False), - 21: ExpectedOutput(1, 6, 186, n_spots_homogeneous_solid, False), - 22: ExpectedOutput(1, 5, 186, n_spots_homogeneous_solid, False), - 23: ExpectedOutput(1, 4, 186, n_spots_homogeneous_solid, False), - 24: ExpectedOutput(1, 4, 187, n_spots_homogeneous_solid, False), - 30: ExpectedOutput(3, 8, 185, n_spots_layered_solid, True), - 31: ExpectedOutput(3, 6, 185, n_spots_layered_solid, True), - 40: ExpectedOutput(3, 8, 186, n_spots_layered_solid, True), - 41: ExpectedOutput(3, 6, 186, n_spots_layered_solid, True), - 50: ExpectedOutput(3, 8, 190, n_spots_layered_solid, True), - 51: ExpectedOutput(3, 6, 190, n_spots_layered_solid, True), + 1: ExpectedOutput(3, 4, 181, n_spots_shell, True, 4), + 2: ExpectedOutput(3, 3, 181, n_spots_shell, True, 3), + 3: ExpectedOutput(3, 4, 281, n_spots_shell, True, 4), + 4: ExpectedOutput(3, 3, 281, n_spots_shell, True, 3), + 10: ExpectedOutput(1, 8, 185, n_spots_homogeneous_solid, False, -1), + 11: ExpectedOutput(1, 6, 185, n_spots_homogeneous_solid, False, -1), + 12: ExpectedOutput(1, 5, 185, n_spots_homogeneous_solid, False, -1), + 13: ExpectedOutput(1, 4, 185, n_spots_homogeneous_solid, False, -1), + 20: ExpectedOutput(1, 8, 186, n_spots_homogeneous_solid, False, -1), + 21: ExpectedOutput(1, 6, 186, n_spots_homogeneous_solid, False, -1), + 22: ExpectedOutput(1, 5, 186, n_spots_homogeneous_solid, False, -1), + 23: ExpectedOutput(1, 4, 186, n_spots_homogeneous_solid, False, -1), + 24: ExpectedOutput(1, 4, 187, n_spots_homogeneous_solid, False, -1), + 30: ExpectedOutput(3, 8, 185, n_spots_layered_solid, True, 4), + 31: ExpectedOutput(3, 6, 185, n_spots_layered_solid, True, 3), + 40: ExpectedOutput(3, 8, 186, n_spots_layered_solid, True, 4), + 41: ExpectedOutput(3, 6, 186, n_spots_layered_solid, True, 3), + 50: ExpectedOutput(3, 8, 190, n_spots_layered_solid, True, 4), + 51: ExpectedOutput(3, 6, 190, n_spots_layered_solid, True, 3), } def check_output(rst_file, expected_output): @@ -184,6 +188,8 @@ def check_output(rst_file, expected_output): corner_nodes_per_layer = element_info.n_corner_nodes else: corner_nodes_per_layer = element_info.n_corner_nodes / 2 + + assert corner_nodes_per_layer == element_info.number_of_nodes_per_spot_plane assert ( num_elementary_data == corner_nodes_per_layer * element_info.n_spots * element_info.n_layers @@ -205,32 +211,32 @@ def check_output(rst_file, expected_output): ) -def test_document_error_cases_indices(dpf_server): +def get_element_info_provider_for_rst(rst_file, server): TEST_DATA_ROOT_DIR = pathlib.Path(__file__).parent / "data" + rst_path = TEST_DATA_ROOT_DIR / "all_element_types" / rst_file - def get_layup_info_for_rst(rst_file): - rst_path = TEST_DATA_ROOT_DIR / "all_element_types" / rst_file + if not server.local_server: + rst_path = upload_file_to_unique_tmp_folder(rst_path, server=server) - if not dpf_server.local_server: - rst_path = upload_file_to_unique_tmp_folder(rst_path, server=dpf_server) + rst_data_source = dpf.DataSources(rst_path) - rst_data_source = dpf.DataSources(rst_path) + mesh_provider = dpf.Operator("MeshProvider") + mesh_provider.inputs.data_sources(rst_data_source) + mesh: MeshedRegion = mesh_provider.outputs.mesh() - mesh_provider = dpf.Operator("MeshProvider") - mesh_provider.inputs.data_sources(rst_data_source) - mesh: MeshedRegion = mesh_provider.outputs.mesh() + with pytest.raises(RuntimeError) as exc_info: + layup_info = get_element_info_provider(mesh, stream_provider_or_data_source=rst_data_source) + assert str(exc_info.value).startswith("Missing property field in mesh") + material_property_field, layer_indices_property_field = get_layup_property_fields() + mesh.set_property_field("element_layered_material_ids", material_property_field) + mesh.set_property_field("element_layer_indices", layer_indices_property_field) + return get_element_info_provider(mesh, stream_provider_or_data_source=rst_data_source) - with pytest.raises(RuntimeError) as exc_info: - layup_info = get_element_info_provider( - mesh, stream_provider_or_data_source=rst_data_source - ) - assert str(exc_info.value).startswith("Missing property field in mesh") - material_property_field, layer_indices_property_field = get_layup_property_fields() - mesh.set_property_field("element_layered_material_ids", material_property_field) - mesh.set_property_field("element_layer_indices", layer_indices_property_field) - return get_element_info_provider(mesh, stream_provider_or_data_source=rst_data_source) - layup_info = get_layup_info_for_rst("model_with_all_element_types_minimal_output.rst") +def test_document_error_cases_indices(dpf_server): + layup_info = get_element_info_provider_for_rst( + "model_with_all_element_types_minimal_output.rst", dpf_server + ) for element_id in get_element_ids().layered: with pytest.raises(RuntimeError) as exc_info: @@ -248,7 +254,9 @@ def get_layup_info_for_rst(rst_file): "Computation of indices is not supported for non-layered elements." ) - layup_info = get_layup_info_for_rst("model_with_all_element_types_all_output.rst") + layup_info = get_element_info_provider_for_rst( + "model_with_all_element_types_all_output.rst", dpf_server + ) for element_id in get_element_ids().non_layered: with pytest.raises(RuntimeError) as exc_info: @@ -279,7 +287,9 @@ def get_layup_info_for_rst(rst_file): selected_indices = get_selected_indices_by_dpf_material_ids(element_info, [5]) assert len(selected_indices) == 0 - layup_info = get_layup_info_for_rst("model_with_all_element_types_all_except_mid_output.rst") + layup_info = get_element_info_provider_for_rst( + "model_with_all_element_types_all_except_mid_output.rst", dpf_server + ) for element_id in get_element_ids().layered: with pytest.raises(RuntimeError) as exc_info: @@ -288,3 +298,112 @@ def get_layup_info_for_rst(rst_file): assert str(exc_info.value).startswith( "Spot index 2 is greater or equal to the number of spots" ) + + +def test_select_indices_all_element_types(dpf_server): + """ + Test get_selected_indices for all types of layered elements. + + The test verifies the indices for the first layer, 2nd layer, and + 2nd layer in combination with spot TOP. + + Note: Non-layered elements are not supported by get_selected_indices + """ + ref_indices_layer_0 = { + 1: np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), # 4 node shell181, 3 layers, 3 spots + 2: np.array([0, 1, 2, 3, 4, 5, 6, 7, 8]), # 3 node shell181, 3 layers, 2 spots + 3: np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), # 8 node shell281, 3 layers, 3 spots + 4: np.array([0, 1, 2, 3, 4, 5, 6, 7, 8]), # 6 node shell281, 3 layers, 2 spots + 30: np.array([0, 1, 2, 3, 4, 5, 6, 7]), # 8 node solid185 + 31: np.array([0, 1, 2, 3, 4, 5]), # 6 node solid185 + 40: np.array([0, 1, 2, 3, 4, 5, 6, 7]), # 20 node solid186 + 41: np.array([0, 1, 2, 3, 4, 5]), # 15 node solid186 + 50: np.array([0, 1, 2, 3, 4, 5, 6, 7]), # 8 node solid190 + 51: np.array([0, 1, 2, 3, 4, 5]), # 6 node solid190 + } + + element_info_provider = get_element_info_provider_for_rst( + "model_with_all_element_types_all_output.rst", dpf_server + ) + element_ids = get_element_ids() + for elem_id in element_ids.all: + element_info = element_info_provider.get_element_info(elem_id) + # get_selected_indices is only supported for layered elements + if element_info.is_layered: + # All indices of the first layer + indices = get_selected_indices(element_info, layers=[0]) + assert ( + indices == ref_indices_layer_0[elem_id] + ).all(), f"{element_info}, {indices} != {ref_indices_layer_0[elem_id]}" + + # All indices of the second layer via it's material ID + material_id = element_info.dpf_material_ids[1] # this is equivalent to the second layer + indices = get_selected_indices_by_dpf_material_ids(element_info, list([material_id])) + + # Offset indices for the second layer + ref_2nd_layer = ref_indices_layer_0[elem_id] + max(ref_indices_layer_0[elem_id]) + 1 + assert ( + indices == ref_2nd_layer + ).all(), f"{element_info}, i{indices} != {ref_2nd_layer}" + + # Indices of the second layer and the top spot + indices = get_selected_indices(element_info, layers=[1], spots=[Spot.TOP]) + num_indices = element_info.number_of_nodes_per_spot_plane + if element_info.is_shell: + # The order of the spots is bot, top, middle for shells + # So the indices in the middle are used to retrieve the data at the top + ref_2nd_layer_top = ref_2nd_layer[num_indices:-num_indices] + else: + # Layered solids have only bottom and top. + ref_2nd_layer_top = ref_2nd_layer[-num_indices:] + assert ( + indices == ref_2nd_layer_top + ).all(), f"{element_info}, {indices} != {ref_2nd_layer_top}" + + +def test_get_element_info_all_element_types(dpf_server): + """ + Test get_element_info for all element types. + + The layered elements have one layer only. In this case, the dpf fields do not have data + pointers. In addition, the analysis_ply_layer_indices property field is not available + because the model is loaded from an RST file without a lay-up definition file. + """ + + if version_older_than(dpf_server, "8.0"): + pytest.xfail( + "Not supported because section data from RST is not implemented before version 8.0." + ) + + model_path = pathlib.Path(__file__).parent / "data" / "all_element_types" + + rst_file = model_path / "model_with_all_element_types_all_output_1_layer.rst" + mat_xml_file = model_path / "model_with_all_element_types_all_output_1_layer_material.xml" + + composite_files = ContinuousFiberCompositesFiles( + rst=[rst_file], + composite={}, + engineering_data=mat_xml_file, + ) + + composite_model = CompositeModel( + composite_files, server=dpf_server, default_unit_system=unit_systems.solver_mks + ) + + expected_indices = { + 1: np.array([4, 5, 6, 7], dtype=np.int64), + 2: np.array([3, 4, 5], dtype=np.int64), + 3: np.array([4, 5, 6, 7], dtype=np.int64), + 4: np.array([3, 4, 5], dtype=np.int64), + 30: np.array([4, 5, 6, 7], dtype=np.int64), + 31: np.array([3, 4, 5], dtype=np.int64), + 40: np.array([4, 5, 6, 7], dtype=np.int64), + 41: np.array([3, 4, 5], dtype=np.int64), + 50: np.array([4, 5, 6, 7], dtype=np.int64), + 51: np.array([3, 4, 5], dtype=np.int64), + } + + for element_id, ref_indices in expected_indices.items(): + element_info = composite_model.get_element_info(element_id) + indices = get_selected_indices(element_info, layers=[0], spots=[Spot.TOP]) + assert (indices == ref_indices).all(), f"{element_info}, {indices} != {ref_indices}" diff --git a/tests/performance_test.py b/tests/performance_test.py index bd53b06a6..b8d62cc30 100644 --- a/tests/performance_test.py +++ b/tests/performance_test.py @@ -27,7 +27,7 @@ import numpy as np import pytest -from ansys.dpf.composites._indexer import FieldIndexerWithDataPointer +from ansys.dpf.composites._indexer import get_field_indexer from ansys.dpf.composites.data_sources import CompositeDefinitionFiles, get_composites_data_sources from ansys.dpf.composites.layup_info import ( LayupPropertiesProvider, @@ -335,7 +335,7 @@ def test_performance_flat(dpf_server): all_data = np.full((setup_result.field.elementary_data_count, 11), -1.0) start_index = 0 - indexer_data = FieldIndexerWithDataPointer(setup_result.field) + indexer_data = get_field_indexer(setup_result.field) timer.add("indexer") with setup_result.mesh.elements.connectivities_field.as_local_field() as local_connectivity: @@ -368,7 +368,7 @@ def test_performance_flat(dpf_server): flat_layer_indices = unraveled_indices[0] flat_spot_indices = unraveled_indices[1] flat_node_indices = unraveled_indices[2] - element_data = indexer_data.by_id(element_id) + element_data = indexer_data.by_id_as_array(element_id) nodes = np.array(local_connectivity.get_entity_data_by_id(element_id)) num_elementary_data = ( diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 37ff358d7..c989b27a7 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -24,4 +24,4 @@ def test_pkg_version(): - assert __version__ == "0.6.0" + assert __version__ == "0.6.1"