From 309bfbf94950caa0b288b8afcc97fcd64437402e Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Mon, 16 May 2022 12:37:58 +0530 Subject: [PATCH] Field data doc. --- doc/source/api/core/solver/fielddata.rst | 248 ++++++++++++++++ doc/source/api/core/solver/index.rst | 3 +- src/ansys/fluent/core/services/field_data.py | 285 ++++++++++++++++--- src/ansys/fluent/core/session.py | 2 +- tests/test_post.py | 50 +++- 5 files changed, 536 insertions(+), 52 deletions(-) create mode 100644 doc/source/api/core/solver/fielddata.rst diff --git a/doc/source/api/core/solver/fielddata.rst b/doc/source/api/core/solver/fielddata.rst new file mode 100644 index 000000000000..e0ce68041026 --- /dev/null +++ b/doc/source/api/core/solver/fielddata.rst @@ -0,0 +1,248 @@ +.. _ref_field_data: + +Field Data +========== +The ``FieldData`` module provides access to Fluent surface, scalar and vector field +data. It provides two sets of APIs. + +#. APIs to get multiple fields in a single request. +#. APIs to get one field per request. + + +++++++++++++++++++++++++++++ +Muliple fields in a request +++++++++++++++++++++++++++++ +In this approach, requests for multiple fields are combined in a single request and +data for all fields is received in a single response. + +* Request + + Requests for multiple fields can be combined in a single request via + `add_get__request` APIs. + + * ``add_get_surfaces_request`` + + * To add surfaces request + + * ``add_get_scalar_fields_request`` + + * To add scalar fields request + + * ``add_get_vector_fields_request`` + + * To add vector fields request + +* Response + + All requested fields are returned in a single response with ``get_fields`` API. It provides + the dictionary containing the requested fields as numpy array in the following order: + + * tag_id [int]-> surface_id [int] -> field_name [str] -> field_data[np.array] + + +Tag Id +^^^^^^^ + +Tag id is generated by applying `bitwise or` on all tags for a request. Following +is the list of supported tags and their values: + +* OVERSET_MESH: 1, +* ELEMENT_LOCATION: 2, +* NODE_LOCATION: 4, +* BOUNDARY_VALUES: 8, + +So if scalar field data is requested for element location[2] then tag_id in +the dictionary will be 2. Similarly if boundary values[8] are requested for +node location[4] then tag_id will be (4|8) i.e. 12. + +Surface ID +^^^^^^^^^^^ + +The Surface ID is the same one as passed in the request. + +Field Name +^^^^^^^^^^^ + +For a request multiple fields are returned. Number of fields depends upon the request type. + +* Surface request + + Response will contain any of the following fields depending upon the request arguments. + + * faces + + * Contains faces connectivity + + * vertices + + * Contains node coordinates + + * centroid + + * Contains face centroids + + * face-normal + + * Contains face normals + +* Scalar field request + + Response will contain a single field with the same name as the scalar field name passed in the request. + +* vector field request + + Response will contain two fields + + * Vector field, with name same as vector field name passed in the request. + * vector-scale. + + +Example +^^^^^^^ + +.. code-block:: python + + #Get field data + field_data = session.field_data + + #Add requests + + #Data for surfaces for following requests will be returned in tag_id 0. As there is no tag. + + field_data.add_get_surfaces_request(surface_ids=[1], provide_vertices=True, + provide_faces=False, provide_faces_centroid=True + ) + + field_data.add_get_surfaces_request(surface_ids=[2], provide_vertices=True, + provide_faces=True + ) + + #Data for tempaeraure for following request will be returned in tag_id 12 i.e. 4|8. + field_data.add_get_scalar_fields_request(surface_ids=[1,2], field_name="temperature", + node_value=True, boundary_value=True + ) + + #Data for tempaeraure for following request will be returned in tag_id 4. + field_data.add_get_scalar_fields_request(surface_ids=[3], field_name="temperature", + node_value=True, boundary_value=False + ) + + #Data for pressure for following request will be returned in tag_id 2. + field_data.add_get_scalar_fields_request(surface_ids=[1,4], field_name="pressure", + node_value=False, boundary_value=False + ) + + #Get fields + + payload_data = field_data.get_fields() + + + #Data will be returned in dictionary with order + #`tag_id [int]-> surface_id [int] -> field_name [str] -> field_data [np.array]` + { + 0:{ + 1:{ + "vertices": np.array #for vertices. + "centroid": np.array #for faces centroid. + }, + 2:{ + "vertices": np.array #for vertices. + "faces": np.array #for faces connectivity. + }, + }, + 12:{ + 1:{ + "temperature": np.array #for temperature at node location with boundary values. + }, + 2:{ + "temperature": np.array #for temperature at node location with boundary values. + }, + }, + 4:{ + 3:{ + "temperature": np.array #for temperature at node location. + } + }, + 2:{ + 1:{ + "pressure": np.array #for pressure at element location. + }, + 4:{ + "pressure": np.array #for pressure at element location. + }, + }, + } + +++++++++++++++++++++++++++++ +One field per request +++++++++++++++++++++++++++++ +In this approach, one field is received for each request. For each field i.e. surface, scalar +and vector there is a separate API. + +#. ``get_surface_data`` + + * To get surface data. + +#. ``get_scalar_field_data`` + + * To get scalar field data. + +#. ``get_vector_field_data`` + + * To get vector field data. + +For surface and scalar field, response contains dictionary of surface ID and numpy array of +the requested field. + +* surface_id [int] -> field[np.array] + +For vector field, response is dictionary of surface ID and Tuple of numpy array of vector field +and vector scale. + +* surface_id [int] -> (vector field [np.array], vector-scale [float]) + +It is important to note that in Fluent, a surface name can be associated with multiple surface +IDs. So response contains surface ID as key of returned dictionary. + +Example +^^^^^^^ + +.. code-block:: python + + from ansys.fluent.core.services.field_data import SurfaceDataType + + #Get field data object + field_data = session.field_data + + #wall surface is associated with two IDs i.e. id1 and id2 + + #Get surface data + vertices = field_data.get_surface_data("wall", SurfaceDataType.Vertices) + #return value>> {id1: np.array, id2: np.array} + normals = field_data.get_surface_data("wall", SurfaceDataType.FacesNormal) + #return value>> {id1: np.array, id2: np.array} + + #Get scalar field data + scalar_field_data = field_data.get_scalar_field_data("wall", "temperature") + #return value>> {id1: np.array, id2: np.array} + + #Get vector field data + vector_field_data = field_data.get_vector_field_data("wall", "velocity") + #return value>> {id1: (np.array, float), id2: (np.array, float)} + + +.. currentmodule:: ansys.fluent.core.services + +.. autosummary:: + :toctree: _autosummary + + +.. automethod:: ansys.fluent.core.services.field_data.FieldData.add_get_surfaces_request +.. automethod:: ansys.fluent.core.services.field_data.FieldData.add_get_scalar_fields_request +.. automethod:: ansys.fluent.core.services.field_data.FieldData.add_get_vector_fields_request +.. automethod:: ansys.fluent.core.services.field_data.FieldData.get_fields + +.. automethod:: ansys.fluent.core.services.field_data.FieldData.get_surface_data +.. automethod:: ansys.fluent.core.services.field_data.FieldData.get_scalar_field_data +.. automethod:: ansys.fluent.core.services.field_data.FieldData.get_vector_field_data + \ No newline at end of file diff --git a/doc/source/api/core/solver/index.rst b/doc/source/api/core/solver/index.rst index 7a40186b9f86..9eb7c802be9f 100644 --- a/doc/source/api/core/solver/index.rst +++ b/doc/source/api/core/solver/index.rst @@ -12,4 +12,5 @@ Solver :hidden: settings - tui \ No newline at end of file + tui + fielddata \ No newline at end of file diff --git a/src/ansys/fluent/core/services/field_data.py b/src/ansys/fluent/core/services/field_data.py index af1308294fc9..049c0dd93b01 100644 --- a/src/ansys/fluent/core/services/field_data.py +++ b/src/ansys/fluent/core/services/field_data.py @@ -1,7 +1,8 @@ """Wrappers over FieldData grpc service of Fluent.""" +from enum import IntEnum from functools import reduce -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Tuple, Union import grpc import numpy as np @@ -60,7 +61,7 @@ class FieldInfo: """ def __init__(self, service: FieldDataService): - self.__service = service + self._service = service def get_range( self, field: str, node_value: bool = False, surface_ids: List[int] = [] @@ -71,12 +72,12 @@ def get_range( request.surfaceid.extend( [FieldDataProtoModule.SurfaceId(id=int(id)) for id in surface_ids] ) - response = self.__service.get_range(request) + response = self._service.get_range(request) return [response.minimum, response.maximum] def get_fields_info(self) -> dict: request = FieldDataProtoModule.GetFieldsInfoRequest() - response = self.__service.get_fields_info(request) + response = self._service.get_fields_info(request) return { field_info.displayName: { "solver_name": field_info.solverName, @@ -88,7 +89,7 @@ def get_fields_info(self) -> dict: def get_vector_fields_info(self) -> dict: request = FieldDataProtoModule.GetVectorFieldsInfoRequest() - response = self.__service.get_vector_fields_info(request) + response = self._service.get_vector_fields_info(request) return { vector_field_info.displayName: { "x-component": vector_field_info.xComponent, @@ -100,7 +101,7 @@ def get_vector_fields_info(self) -> dict: def get_surfaces_info(self) -> dict: request = FieldDataProtoModule.GetSurfacesInfoResponse() - response = self.__service.get_surfaces_info(request) + response = self._service.get_surfaces_info(request) info = { surface_info.surfaceName: { "surface_id": [surf.id for surf in surface_info.surfaceId], @@ -113,41 +114,17 @@ def get_surfaces_info(self) -> dict: return info -class FieldData: - """Provides access to Fluent field data on surfaces. +class SurfaceDataType(IntEnum): + """Surface data type.""" - Methods - ------- - add_get_surfaces_request( - surface_ids: List[int], - overset_mesh: bool = False, - provide_vertices=True, - provide_faces=True, - provide_faces_centroid=False, - provide_faces_normal=False, - ) -> None - Add request to get surfaces data i.e. vertices, faces connectivity, - centroids and normals. + Vertices = 1 + FacesConnectivity = 2 + FacesNormal = 3 + FacesCentroid = 4 - add_get_scalar_fields_request( - surface_ids: List[int], - scalar_field: str, - node_value: Optional[bool] = True, - boundary_value: Optional[bool] = False, - ) -> None - Add request to get scalar field data on surfaces. - add_get_vector_fields_request( - surface_ids: List[int], - vector_field: Optional[str] = "velocity" - ) -> None - Add request to get vector field data on surfaces. - - get_fields(self) -> Dict[int, Dict] - Provide data for previously added requests. - Data is returned as dictionary of dictionaries in following structure: - tag_id [int]-> surface_id [int] -> field_name [str] -> field_data - """ +class FieldData: + """Provides access to Fluent field data on surfaces.""" # data mapping _proto_field_type_to_np_data_type = { @@ -165,8 +142,9 @@ class FieldData: FieldDataProtoModule.PayloadTag.BOUNDARY_VALUES: 8, } - def __init__(self, service: FieldDataService): - self.__service = service + def __init__(self, service: FieldDataService, field_info: FieldInfo): + self._service = service + self._field_info = field_info self._fields_request = None def _extract_fields(self, chunk_iterator): @@ -236,15 +214,189 @@ def _get_fields_request(self): ) return self._fields_request + def get_surface_data( + self, + surface_name: str, + data_type: SurfaceDataType, + overset_mesh: Optional[bool] = False, + ) -> Dict[int, np.array]: + """Get surface data i.e. vertices, faces connectivity, centroids and + normals. + + Parameters + ---------- + surface_name : str + Surface name for surface data. + data_type : SurfaceDataType + SurfaceDataType Enum member. + overset_mesh : bool, optional + If set to True overset mesh will be provided. + + Returns + -------- + Dict[int, np.array] + Dictionary containing map of surface id to surface data. + """ + surface_ids = self._field_info.get_surfaces_info()[surface_name]["surface_id"] + self._get_fields_request().surfaceRequest.extend( + [ + FieldDataProtoModule.SurfaceRequest( + surfaceId=surface_id, + oversetMesh=overset_mesh, + provideFaces=data_type == SurfaceDataType.FacesConnectivity, + provideVertices=data_type == SurfaceDataType.Vertices, + provideFacesCentroid=data_type == SurfaceDataType.FacesCentroid, + provideFacesNormal=data_type == SurfaceDataType.FacesNormal, + ) + for surface_id in surface_ids + ] + ) + enum_to_field_name = { + SurfaceDataType.FacesConnectivity: "faces", + SurfaceDataType.Vertices: "vertices", + SurfaceDataType.FacesCentroid: "centroid", + SurfaceDataType.FacesNormal: "face-normal", + } + request = self._get_fields_request() + self._fields_request = None + tag_id = 0 + if overset_mesh: + tag_id = self._payloadTags[FieldDataProtoModule.PayloadTag.OVERSET_MESH] + fields = self._extract_fields(self._service.get_fields(request))[tag_id] + return { + surface_id: fields[surface_id][enum_to_field_name[data_type]] + for surface_id in surface_ids + } + + def get_scalar_field_data( + self, + surface_name: str, + field_name: str, + node_value: Optional[bool] = True, + boundary_value: Optional[bool] = False, + ) -> Dict[int, np.array]: + """Get scalar field data on a surface. + + Parameters + ---------- + surface_name : str + Surface name, for scalar field data. + field_name : str + Scalar field name. + node_value : bool, optional + if set to True data will be provided for nodal location otherwise + data will be provided for element location. + boundary_value : bool, optional + if set to True, no slip velocity will be provided at wall boundaries. + + Returns + -------- + Dict[int, np.array] + Dictionary containing map of surface id to scalar field. + """ + surface_ids = self._field_info.get_surfaces_info()[surface_name]["surface_id"] + self._get_fields_request().scalarFieldRequest.extend( + [ + FieldDataProtoModule.ScalarFieldRequest( + surfaceId=surface_id, + scalarFieldName=field_name, + dataLocation=FieldDataProtoModule.DataLocation.Nodes + if node_value + else FieldDataProtoModule.DataLocation.Elements, + provideBoundaryValues=boundary_value, + ) + for surface_id in surface_ids + ] + ) + request = self._get_fields_request() + self._fields_request = None + tag_id = 0 + if node_value: + tag_id = self._payloadTags[FieldDataProtoModule.PayloadTag.NODE_LOCATION] + else: + tag_id = self._payloadTags[FieldDataProtoModule.PayloadTag.ELEMENT_LOCATION] + if boundary_value: + tag_id = ( + tag_id + | self._payloadTags[FieldDataProtoModule.PayloadTag.BOUNDARY_VALUES] + ) + fields = self._extract_fields(self._service.get_fields(request))[tag_id] + return { + surface_id: fields[surface_id][field_name] for surface_id in surface_ids + } + + def get_vector_field_data( + self, + surface_name: str, + vector_field: Optional[str] = "velocity", + ) -> Dict[int, Tuple[np.array, float]]: + """Get vector field data on surface. + + Parameters + ---------- + surface_name : str + Surface name, for vector field data. + vector_field : str, optional + Vector field name. + + Returns + -------- + Dict[int, Tuple[np.array, float]] + Dictionary containing map of surface id to Tuple of vector field and vector scale. + """ + surface_ids = self._field_info.get_surfaces_info()[surface_name]["surface_id"] + self._get_fields_request().vectorFieldRequest.extend( + [ + FieldDataProtoModule.VectorFieldRequest( + surfaceId=surface_id, + vectorFieldName=vector_field, + ) + for surface_id in surface_ids + ] + ) + request = self._get_fields_request() + self._fields_request = None + tag_id = 0 + fields = self._extract_fields(self._service.get_fields(request)) + return { + surface_id: ( + fields[tag_id][surface_id][vector_field], + fields[tag_id][surface_id]["vector-scale"][0], + ) + for surface_id in surface_ids + } + def add_get_surfaces_request( self, surface_ids: List[int], - overset_mesh: bool = False, - provide_vertices=True, - provide_faces=True, - provide_faces_centroid=False, - provide_faces_normal=False, + overset_mesh: Optional[bool] = False, + provide_vertices: Optional[bool] = True, + provide_faces: Optional[bool] = True, + provide_faces_centroid: Optional[bool] = False, + provide_faces_normal: Optional[bool] = False, ) -> None: + """Add request to get surfaces data i.e. vertices, faces connectivity, + centroids and normals. + + Parameters + ---------- + surface_ids : List[int] + List of surface ids, for surface data. + overset_mesh : bool, optional + If set to True overset mesh will be provided. + provide_vertices : bool, optional + if set to True vertices i.e. node coordinates will be provided. + provide_faces : bool, optional + if set to True faces connectivity will be provided. + provide_faces_centroid : bool, optional + if set to True faces centroid will be provided. + provide_faces_normal : bool, optional + if set to True faces normal will be provided. + + Returns + -------- + None + """ self._get_fields_request().surfaceRequest.extend( [ FieldDataProtoModule.SurfaceRequest( @@ -266,6 +418,24 @@ def add_get_scalar_fields_request( node_value: Optional[bool] = True, boundary_value: Optional[bool] = False, ) -> None: + """Add request to get scalar field data on surfaces. + + Parameters + ---------- + surface_ids : List[int] + List of surface ids, for scalar field data. + field_name : str + Scalar field name. + node_value : bool, optional + if set to True data will be provided for nodal location otherwise + data will be provided for element location. + boundary_value : bool, optional + if set to True, no slip velocity will be provided at wall boundaries. + + Returns + -------- + None + """ self._get_fields_request().scalarFieldRequest.extend( [ FieldDataProtoModule.ScalarFieldRequest( @@ -285,6 +455,19 @@ def add_get_vector_fields_request( surface_ids: List[int], vector_field: Optional[str] = "velocity", ) -> None: + """Add request to get vector field data on surfaces. + + Parameters + ---------- + surface_ids : List[int] + List of surface ids, for vector field data. + vector_field : str, optional + Vector field name. + + Returns + -------- + None + """ self._get_fields_request().vectorFieldRequest.extend( [ FieldDataProtoModule.VectorFieldRequest( @@ -295,7 +478,15 @@ def add_get_vector_fields_request( ] ) - def get_fields(self) -> Dict[int, Dict]: + def get_fields(self) -> Dict[int, Dict[int, Dict[str, np.array]]]: + """Provide data for previously added requests. + + Returns + ------- + Dict[int, Dict[int, Dict[str, np.array]]] + Data is returned as dictionary of dictionaries in following structure: + tag_id [int]-> surface_id [int] -> field_name [str] -> field_data[np.array] + """ request = self._get_fields_request() self._fields_request = None - return self._extract_fields(self.__service.get_fields(request)) + return self._extract_fields(self._service.get_fields(request)) diff --git a/src/ansys/fluent/core/session.py b/src/ansys/fluent/core/session.py index b1554cf45a6d..ac6f19f1fcd5 100644 --- a/src/ansys/fluent/core/session.py +++ b/src/ansys/fluent/core/session.py @@ -202,7 +202,7 @@ def __init__( self._field_data_service = FieldDataService(self._channel, self._metadata) self.field_info = FieldInfo(self._field_data_service) - self.field_data = FieldData(self._field_data_service) + self.field_data = FieldData(self._field_data_service, self.field_info) self.meshing = Session.Meshing( self._datamodel_service_tui, self._datamodel_service_se diff --git a/tests/test_post.py b/tests/test_post.py index bd965a3974bd..70847218e00b 100644 --- a/tests/test_post.py +++ b/tests/test_post.py @@ -1,9 +1,11 @@ from pathlib import Path import pickle -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union +import numpy as np import pytest +from ansys.fluent.core.services.field_data import SurfaceDataType from ansys.fluent.post.matplotlib import Plots from ansys.fluent.post.pyvista import Graphics @@ -17,9 +19,45 @@ def patch_mock_data_extractor(mocker) -> None: class MockFieldData: - def __init__(self, solver_data): + def __init__(self, solver_data, field_info): self._session_data = solver_data self._request_to_serve = {"surf": [], "scalar": [], "vector": []} + self._field_info = field_info + + def get_surface_data( + self, + surface_name: str, + data_type: Union[SurfaceDataType, int], + overset_mesh: Optional[bool] = False, + ) -> Dict: + surfaces_info = self._field_info().get_surfaces_info() + surface_ids = surfaces_info[surface_name]["surface_id"] + self._request_to_serve["surf"].append( + ( + surface_ids, + overset_mesh, + data_type == SurfaceDataType.Vertices, + data_type == SurfaceDataType.FacesConnectivity, + data_type == SurfaceDataType.FacesCentroid, + data_type == SurfaceDataType.FacesNormal, + ) + ) + enum_to_field_name = { + SurfaceDataType.FacesConnectivity: "faces", + SurfaceDataType.Vertices: "vertices", + SurfaceDataType.FacesCentroid: "centroid", + SurfaceDataType.FacesNormal: "face-normal", + } + + tag_id = 0 + if overset_mesh: + tag_id = self._payloadTags[FieldDataProtoModule.PayloadTag.OVERSET_MESH] + return { + surface_id: self._session_data["fields"][tag_id][surface_id][ + enum_to_field_name[data_type] + ] + for surface_id in surface_ids + } def add_get_surfaces_request( self, @@ -132,7 +170,7 @@ def __init__(self, obj=None): MockLocalObjectDataExtractor._session_data ) self.field_data = lambda: MockFieldData( - MockLocalObjectDataExtractor._session_data + MockLocalObjectDataExtractor._session_data, self.field_info ) self.id = lambda: 1 @@ -147,6 +185,10 @@ def test_field_api(): v["surface_id"][0] for k, v in field_info.get_surfaces_info().items() ] + # Get vertices + vertices_data = field_data.get_surface_data("wall", SurfaceDataType.Vertices) + + # Get multiple fields field_data.add_get_surfaces_request( surfaces_id[:1], provide_vertices=True, @@ -166,6 +208,8 @@ def test_field_api(): element_location_tag = 2 element_data = fields[element_location_tag][surfaces_id[0]]["temperature"] + # Compare vertices obtained by different APIs + np.testing.assert_array_equal(vertices, vertices_data[next(iter(vertices_data))]) assert len(vertices) == len(node_data) * 3 assert len(centroid) == len(element_data) * 3