Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions doc/sphinx_gallery_tutorials/data_structures/data_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,71 @@
my_scratch_property_field.scoping.ids = [1, 2]
print(my_scratch_property_field)

###############################################################################
# ElementalNodal field
# ^^^^^^^^^^^^^^^^^^^^
#
# At the ``elemental_nodal`` location, each element can have a different number
# of data points — one row per integration node of the element. Use
# :func:`append()<ansys.dpf.core.field.Field.append>` to populate the field
# entity by entity: DPF records the start index of each entity's block in the
# flat data array automatically via
# :attr:`entity_data_offsets<ansys.dpf.core.field.Field.entity_data_offsets>`.

import numpy as np

# Create an ElementalNodal field populated entity by entity
my_en_field = dpf.Field(location=dpf.locations.elemental_nodal)
# Element 10: 2 nodes × 3 components → 6 values
my_en_field.append([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], 10)
# Element 20: 1 integration node × 3 components → 3 values
my_en_field.append([7.0, 8.0, 9.0], 20)
# Element 30: 3 integration nodes × 3 components → 9 values
my_en_field.append([10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0], 30)

# entity_data_offsets gives the start index in the flat data array for each entity
print("entity_data_offsets:", my_en_field.entity_data_offsets)
# get_entity_data returns a 2D array shaped (n_integration_nodes, n_components)
print("element 10 shape:", my_en_field.get_entity_data(index=0).shape)
print("element 20 shape:", my_en_field.get_entity_data(index=1).shape)
print("element 30 shape:", my_en_field.get_entity_data(index=2).shape)

###############################################################################
# For high-performance bulk creation, assign all data at once and set
# :attr:`entity_data_offsets<ansys.dpf.core.field.Field.entity_data_offsets>`
# explicitly instead of appending entity by entity.

# Assign scoping, flat data, and offsets in one step
my_en_field_bulk = dpf.Field(location=dpf.locations.elemental_nodal)
my_en_field_bulk.scoping.ids = [10, 20, 30]
my_en_field_bulk.data = np.array(
[
1.0,
2.0,
3.0,
4.0,
5.0,
6.0, # element 10: 2 nodes × 3 comp
7.0,
8.0,
9.0, # element 20: 1 node × 3 comp
10.0,
11.0,
12.0,
13.0,
14.0,
15.0,
16.0,
17.0,
18.0, # element 30: 3 nodes × 3 comp
]
)
# Each value is the flat-array index where that entity's data block begins
my_en_field_bulk.entity_data_offsets = [0, 6, 9]
print("element 10 shape:", my_en_field_bulk.get_entity_data(index=0).shape)
print("element 20 shape:", my_en_field_bulk.get_entity_data(index=1).shape)
print("element 30 shape:", my_en_field_bulk.get_entity_data(index=2).shape)

###############################################################################
# Create fields with the fields_factory
# --------------------------------------
Expand Down Expand Up @@ -377,6 +442,8 @@
print("shape\n", my_temp_field.shape, "\n")
# Unit (only available on Field, not StringField or PropertyField)
print("unit\n", my_temp_field.unit, "\n")
# Start index of each entity's data block in the flat data array (ElementalNodal fields)
print("entity_data_offsets\n", my_en_field.entity_data_offsets, "\n")

###############################################################################
# StringField
Expand Down
6 changes: 5 additions & 1 deletion src/ansys/dpf/core/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,10 @@ def _get_data_pointer(self):
def _set_data_pointer(self, data):
return self._api.csfield_set_data_pointer(self, _get_size_of_list(data), data)

# Keep the private alias for backward compatibility (used in deep_copy and
# by external code that may already reference _data_pointer directly).
_data_pointer = property(_get_data_pointer, _set_data_pointer)

def _get_data(self, np_array=True):
try:
vec = dpf_vector.DPFVectorDouble(owner=self)
Expand Down Expand Up @@ -940,7 +944,7 @@ def deep_copy(self, server=None):
f.location = self.location
f.field_definition = self.field_definition.deep_copy(server)
with suppress(Exception):
f._data_pointer = self._data_pointer
f.entity_data_offsets = self.entity_data_offsets

# A field can only have ONE support (mesh OR time_freq_support).
# Setting one overwrites the other, so they must be mutually exclusive.
Expand Down
65 changes: 65 additions & 0 deletions src/ansys/dpf/core/field_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,43 @@ def _data_pointer(self, data):
def _set_data_pointer(self, data):
pass

@property
def data_pointer(self):
"""Start indices of each entity's data in the flat :attr:`data` array.

For fields where every entity has the same number of components
(``nodal``, ``elemental``, scalar, 3D-vector, …) this array is empty.
For fields with variable-size entity data - such as an ``elemental_nodal``
:class:`Field <ansys.dpf.core.field.Field>`, or a
:class:`PropertyField <ansys.dpf.core.property_field.PropertyField>` storing
connectivity - ``data_pointer[i]`` is the flat-array index where
entity *i*'s data block begins.

Returns
-------
numpy.ndarray
Array of start indices, one per entity. Empty when all entities
have the same number of components.

See Also
--------
:meth:`get_entity_data`, :meth:`append`
"""
return self._get_data_pointer()

@data_pointer.setter
def data_pointer(self, value):
self._set_data_pointer(value)

@property
def entity_data_offsets(self):
"""Alias for :attr:`data_pointer`."""
return self.data_pointer

@entity_data_offsets.setter
def entity_data_offsets(self, value):
self.data_pointer = value

@property
def data(self):
"""Data in the field as an array.
Expand Down Expand Up @@ -860,6 +897,34 @@ def _data_pointer(self, data):
if self._has_data_pointer == False and len(data) > 0:
self._has_data_pointer = True

@property
def data_pointer(self):
"""Start indices of each entity's data in the flat :attr:`data` array.

See :attr:`_FieldBase.data_pointer` for full documentation.
"""
return np.array(self._data_pointer_copy)

@data_pointer.setter
@_setter
def data_pointer(self, data):
if isinstance(data, (np.ndarray, np.generic)):
self._data_pointer_copy = data.tolist()
else:
self._data_pointer_copy = data
if self._has_data_pointer == False and len(data) > 0:
self._has_data_pointer = True

@property
def entity_data_offsets(self):
"""Alias for :attr:`data_pointer`."""
return self.data_pointer

@entity_data_offsets.setter
@_setter
def entity_data_offsets(self, data):
self.data_pointer = data

@property
def scoping_ids(self):
"""Scoping IDs of the field.
Expand Down
4 changes: 2 additions & 2 deletions src/ansys/dpf/core/plotter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1885,14 +1885,14 @@ def plot_contour( # noqa: PLR0912, PLR0915, C901
else:
overall_data = np.full(location_data_len, np.nan)

# field._data_pointer gives the first index of each entity data
# field.entity_data_offsets gives the first index of each entity data
# (should be of size nb_elements)

for field in fields_container:
ind, mask = mesh_location.map_scoping(field.scoping)
if location == locations.elemental_nodal:
# Rework ind and mask to take into account n_nodes per element
# entity_index_map = field._data_pointer
# entity_index_map = field.entity_data_offsets
n_nodes_list = mesh.get_elemental_nodal_size_list().astype(np.int32)
first_index = np.insert(np.cumsum(n_nodes_list)[:-1], 0, 0).astype(np.int32)
mask_2 = np.asarray(
Expand Down
15 changes: 15 additions & 0 deletions src/ansys/dpf/core/string_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@ def append(self, data: List[str], scopingid: int):
string_list = integral_types.MutableListString(data)
self._api.csstring_field_push_back(self, scopingid, _get_size_of_list(data), string_list)

def _get_data_pointer(self):
from ansys.dpf.core.server_types import GrpcServer, LegacyGrpcServer

if isinstance(self._server, (GrpcServer, LegacyGrpcServer)):
# Remote gRPC servers crash when CSStringField_GetDataPointer is called
# with a shared object handle. Raise NotImplementedError early to avoid
# corrupting the server session for subsequent tests.
raise NotImplementedError(
"StringField.entity_data_offsets (data_pointer) is not supported on remote gRPC servers. "
)
return self._api.csstring_field_get_data_pointer(self, True)

def _set_data_pointer(self, data):
return self._api.csstring_field_set_data_pointer(self, _get_size_of_list(data), data)

def _get_data(self, np_array=True):
try:
vec = dpf_vector.DPFVectorString(owner=self)
Expand Down
6 changes: 3 additions & 3 deletions src/ansys/dpf/core/vtk_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,15 @@ def _dpf_mesh_to_vtk_py( # noqa: PLR0912, PLR0915, C901
coordinates_field = nodes
node_coordinates = nodes.data

elem_size = np.ediff1d(np.append(connectivity._data_pointer, connectivity.shape))
elem_size = np.ediff1d(np.append(connectivity.entity_data_offsets, connectivity.shape))

faces_nodes_connectivity = mesh.property_field("faces_nodes_connectivity")
faces_nodes_connectivity_dp = np.append(
faces_nodes_connectivity._data_pointer, len(faces_nodes_connectivity)
faces_nodes_connectivity.entity_data_offsets, len(faces_nodes_connectivity)
)
elements_faces_connectivity = mesh.property_field("elements_faces_connectivity")
elements_faces_connectivity_dp = np.append(
elements_faces_connectivity._data_pointer, len(elements_faces_connectivity)
elements_faces_connectivity.entity_data_offsets, len(elements_faces_connectivity)
)

insert_ind = np.cumsum(elem_size)
Expand Down
8 changes: 8 additions & 0 deletions src/ansys/dpf/gate/string_field_grpcapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ def csstring_field_set_cscoping(field, scoping):
def csstring_field_push_back(field, EntityId, size, data):
return api_to_call.csfield_push_back(field, EntityId, size, data)

@staticmethod
def csstring_field_get_data_pointer(field, np_array):
return api_to_call.csfield_get_data_pointer(field, np_array)

@staticmethod
def csstring_field_set_data_pointer(field, size, data):
return api_to_call.csfield_set_data_pointer(field, size, data)

@staticmethod
def csstring_field_resize(field, dataSize, scopingSize):
raise api_to_call.csfield_resize(field, dataSize, scopingSize)
Expand Down
14 changes: 7 additions & 7 deletions tests/test_custom_type_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_create_custom_type_field_push_back(server_type):
assert f_scal.scoping.ids[1] == 2


def test_set_get_data_pointer_custom_type_field(server_type):
def test_set_get_entity_data_offsets_custom_type_field(server_type):
field = dpf.core.CustomTypeField(np.float64, nentities=20, server=server_type)
field_def = dpf.core.FieldDefinition(server=server_type)
field_def.dimensionality = dpf.core.Dimensionality({3}, dpf.core.natures.vector)
Expand All @@ -82,9 +82,9 @@ def test_set_get_data_pointer_custom_type_field(server_type):
for i in range(0, 24):
data[i] = i
field.data = data
field._data_pointer = [0, 6, 12, 18]
field.entity_data_offsets = [0, 6, 12, 18]
assert np.allclose(field.data, np.array(data, dtype=float).reshape(8, 3))
assert np.allclose(field._data_pointer, [0, 6, 12, 18])
assert np.allclose(field.entity_data_offsets, [0, 6, 12, 18])
assert np.allclose(field.get_entity_data(0), np.array(range(0, 6)).reshape(2, 3))
assert np.allclose(field.get_entity_data(1), np.array(range(6, 12)).reshape(2, 3))
assert np.allclose(field.get_entity_data(2), np.array(range(12, 18)).reshape(2, 3))
Expand Down Expand Up @@ -142,7 +142,7 @@ def test_mutable_data_custom_type_field(server_clayer):
for i in range(0, 24):
data[i] = i
field.data = data
field._data_pointer = [0, 6, 12, 18]
field.entity_data_offsets = [0, 6, 12, 18]

vec = field.get_entity_data(0)
assert np.allclose(vec, np.array(range(0, 6)).reshape(2, 3))
Expand Down Expand Up @@ -179,15 +179,15 @@ def get_float_field(server_clayer):
for i in range(0, 24):
data[i] = i
field.data = data
field._data_pointer = [0, 6, 12, 18]
field.entity_data_offsets = [0, 6, 12, 18]
return field


def test_mutable_data_pointer_custom_type_field(server_clayer):
def test_mutable_entity_data_offsets_custom_type_field(server_clayer):
float_field = get_float_field(server_clayer)
assert np.allclose(float_field.get_entity_data(0), np.array(range(0, 6)).reshape(2, 3))
assert np.allclose(float_field.get_entity_data(1), np.array(range(6, 12)).reshape(2, 3))
vec = float_field._data_pointer
vec = float_field.entity_data_offsets
vec[1] = 9
vec[2] = 15
vec.commit()
Expand Down
12 changes: 8 additions & 4 deletions tests/test_dpf_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_update_empty_dpf_vector_prop_field(server_type):
prop_field.data = np.zeros((100))
prop_field.scoping.ids = list(range(1, 100))
assert np.allclose(prop_field.get_entity_data(1), [0])
dp = prop_field._data_pointer
dp = prop_field.entity_data_offsets
dp = None
assert np.allclose(prop_field.get_entity_data(1), [0])

Expand All @@ -76,17 +76,21 @@ def test_update_empty_dpf_vector_field(server_type):
field.data = np.zeros((100), dtype=np.double)
field.scoping.ids = list(range(1, 100))
assert np.allclose(field.get_entity_data(1), [0])
dp = field._data_pointer
dp = field.entity_data_offsets
dp = None
assert np.allclose(field.get_entity_data(1), [0])


@pytest.mark.xfail(
reason="StringField.entity_data_offsets requires CSStringField_GetDataPointer_For_DpfVector in DPF server.",
strict=False,
)
def test_update_empty_dpf_vector_string_field(server_type):
string_field = dpf.StringField(server=server_type)
string_field.data = ["high", "goodbye", "hello"]
string_field.scoping.ids = list(range(1, 3))
assert string_field.get_entity_data(1) == ["goodbye"]
dp = string_field._data_pointer
dp = string_field.entity_data_offsets
dp = None
assert string_field.get_entity_data(1) == ["goodbye"]

Expand All @@ -96,7 +100,7 @@ def test_update_empty_dpf_vector_custom_type_field(server_type):
field.data = np.zeros((100), dtype=np.double)
field.scoping.ids = list(range(1, 100))
assert np.allclose(field.get_entity_data(1), [0])
dp = field._data_pointer
dp = field.entity_data_offsets
dp = None
assert np.allclose(field.get_entity_data(1), [0])

Expand Down
Loading
Loading