From 646b0e13f2d1d79dafbeb586751ea43cf2569d5d Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Mon, 24 Jan 2022 13:40:58 +0530 Subject: [PATCH 01/22] Post processing with pyVista --- ansys/fluent/core/core.py | 143 ++++++- ansys/fluent/postprocessing/__init__.py | 2 + .../fluent/postprocessing/pyvista/__init__.py | 2 + .../fluent/postprocessing/pyvista/graphics.py | 350 +++++++++++++++++ .../fluent/postprocessing/pyvista/plotter.py | 356 +++++++++++++++++ ansys/fluent/session.py | 20 +- ansys/fluent/solver/meta.py | 358 +++++++++++++++++- ansys/fluent/solver/tui.py | 6 +- grpc/ansys-api-fluent-v0-0.0.1.tar.gz | Bin 8894 -> 20121 bytes 9 files changed, 1224 insertions(+), 13 deletions(-) create mode 100644 ansys/fluent/postprocessing/__init__.py create mode 100644 ansys/fluent/postprocessing/pyvista/__init__.py create mode 100644 ansys/fluent/postprocessing/pyvista/graphics.py create mode 100644 ansys/fluent/postprocessing/pyvista/plotter.py diff --git a/ansys/fluent/core/core.py b/ansys/fluent/core/core.py index 40517d1163c..f2a38de9db3 100644 --- a/ansys/fluent/core/core.py +++ b/ansys/fluent/core/core.py @@ -5,6 +5,7 @@ import os import keyword from ansys.api.fluent.v0 import datamodel_pb2 as DataModelProtoModule +from ansys.api.fluent.v0 import fielddata_pb2 as FieldDataProtoModule MODULE_NAME_ALIAS = "pyfluent" JOURNAL_FILENAME = None @@ -18,7 +19,7 @@ def convert_value_to_gvalue(val, gval): elif isinstance(val, str): gval.string_value = val elif isinstance(val, list) or isinstance(val, tuple): - # set the one_of to variant_vector_state + # set the one_of to variant_vector_state gval.list_value.values.add() gval.list_value.values.pop() for item in val: @@ -98,10 +99,36 @@ def set_state(self, request): return self.stub.SetState(request, metadata=self.__get_metadata()) def execute_command(self, request): - return self.stub.ExecuteCommand( - request, metadata=self.__get_metadata()) + return self.stub.ExecuteCommand(request, metadata=self.__get_metadata()) + + +class FieldDataService: + def __init__(self, stub, password: str): + self.stub = stub + self.__password = password + + def __get_metadata(self): + return [("password", self.__password)] + def get_surfaces(self, request): + return self.stub.GetSurfaces(request, metadata=self.__get_metadata()) + def get_range(self, request): + return self.stub.GetRange(request, metadata=self.__get_metadata()) + + def get_scalar_field(self, request): + return self.stub.GetScalarField( + request, metadata=self.__get_metadata() + ) + + def get_fields_info(self, request): + return self.stub.GetFieldsInfo(request, metadata=self.__get_metadata()) + + def get_surfaces_info(self, request): + return self.stub.GetSurfacesInfo( + request, metadata=self.__get_metadata() + ) + def start_journal(filename: str): global JOURNAL_FILENAME JOURNAL_FILENAME = filename @@ -190,6 +217,116 @@ def journal_global_fn_call(self, func_name, args=None, kwargs=None): self.__write_to_file(f'{k}={repr(v)}') self.__write_to_file(')\n') +class FieldData: + def __init__(self, service): + self.service = service + + def get_range(self, field, node_value= False, surface_ids=[]): + request = FieldDataProtoModule.GetRangeRequest() + request.fieldName = field + request.nodeValue = node_value + request.surfaceid.extend( + [ + FieldDataProtoModule.SurfaceId(id=int(id)) + for id in surface_ids + ] + ) + response = self.service.get_range(request) + return [response.minimum, response.maximum] + + def get_fields_info(self): + request = FieldDataProtoModule.GetFieldsInfoRequest() + response = self.service.get_fields_info(request) + return { + field_info.displayName: { + "solver_name": field_info.solverName, + "section": field_info.section, + "domain": field_info.domain, + } + for field_info in response.fieldInfo + } + + def get_surfaces_info(self): + request = FieldDataProtoModule.GetSurfacesInfoResponse() + response = self.service.get_surfaces_info(request) + return { + surface_info.surfaceName: { + "surface_id": [ + surf.id for surf in surface_info.surfaceId + ], + "zone_id": surface_info.zoneId.id, + "zone_type": surface_info.zoneType, + "type": surface_info.type, + } + for surface_info in response.surfaceInfo + } + + def _extract_surfaces_data(self, response_iterator): + return [ + { + "vertices": [ + [point.x, point.y, point.z] + for point in response.surfacedata.point + ], + "faces": [ + [len(facet.node)] + [node for node in facet.node] + for facet in response.surfacedata.facet + ], + } + for response in response_iterator + ] + + def get_surfaces(self, surface_ids, overset_mesh=False): + request = FieldDataProtoModule.GetSurfacesRequest() + request.surfaceid.extend( + [ + FieldDataProtoModule.SurfaceId(id=int(id)) + for id in surface_ids + ] + ) + request.oversetMesh = overset_mesh + response_iterator = self.service.get_surfaces(request) + return self._extract_surfaces_data(response_iterator) + + def _extract_scalar_field_data(self, response_iterator): + return [ + { + "vertices": [ + [point.x, point.y, point.z] + for point in response.scalarfielddata.surfacedata.point + ], + "faces": [ + [len(facet.node)] + [node for node in facet.node] + for facet in response.scalarfielddata.surfacedata.facet + ], + "scalar_field": [ + data + for data in response.scalarfielddata.scalarfield.data + ], + "meta_data": response.scalarfielddata.scalarfieldmetadata, + } + for response in response_iterator + ] + + def get_scalar_field( + self, surface_ids, scalar_field, node_value, boundary_value + ): + request = FieldDataProtoModule.GetScalarFieldRequest() + request.surfaceid.extend( + [ + FieldDataProtoModule.SurfaceId(id=int(id)) + for id in surface_ids + ] + ) + request.scalarfield = scalar_field + request.nodevalue = node_value + request.boundaryvalues = boundary_value + response_iterator = self.service.get_scalar_field(request) + return self._extract_scalar_field_data(response_iterator) + + + + class PyMenu: class ExecuteCommandResult: diff --git a/ansys/fluent/postprocessing/__init__.py b/ansys/fluent/postprocessing/__init__.py new file mode 100644 index 00000000000..139597f9cb0 --- /dev/null +++ b/ansys/fluent/postprocessing/__init__.py @@ -0,0 +1,2 @@ + + diff --git a/ansys/fluent/postprocessing/pyvista/__init__.py b/ansys/fluent/postprocessing/pyvista/__init__.py new file mode 100644 index 00000000000..0d7d20b92b8 --- /dev/null +++ b/ansys/fluent/postprocessing/pyvista/__init__.py @@ -0,0 +1,2 @@ +from ansys.fluent.postprocessing.pyvista.plotter import plotter +from ansys.fluent.postprocessing.pyvista.graphics import Graphics diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py new file mode 100644 index 00000000000..ed6b4c302e1 --- /dev/null +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -0,0 +1,350 @@ +from ansys.fluent.solver.meta import ( + PyLocaPropertyMeta, + PyLocalNamedObjectMeta, + Attribute, +) +from ansys.fluent.core.core import FieldData +from ansys.fluent.postprocessing.pyvista.plotter import plotter +from ansys.fluent.session import Session +import sys + + +class Graphics: + """ + Instantiate the graphics objects. + """ + + def __init__(self, session): + self.session = session + self._init_module(self, sys.modules[__name__]) + + def _init_module(self, obj, mod): + for name, cls in mod.__dict__.items(): + if cls.__class__.__name__ == "module": + module = mod.__dict__[name] + cls_obj = type(name, (), {})() + setattr(obj, name, cls_obj) + self._init_module(cls_obj, module) + if cls.__class__.__name__ == "PyLocalNamedObjectMeta": + setattr( + obj, + name, + cls([(name, None)], None, self.session, obj), + ) + + +Session.register_on_exit(lambda: plotter.close()) + + +class mesh(metaclass=PyLocalNamedObjectMeta): + """ + Mesh graphics. + """ + + def display(self): + """ + Displays mesh graphics. + """ + plotter.set_graphics(self) + + class surfaces_list(metaclass=PyLocaPropertyMeta): + """ + List of surfaces for mesh graphics. + """ + + @Attribute + def allowed_values(self): + return list( + FieldData(self.session.field_service) + .get_surfaces_info() + .keys() + ) + + class show_edges(metaclass=PyLocaPropertyMeta): + """ + Show edges for mesh. + """ + + value = False + + +class surface(metaclass=PyLocalNamedObjectMeta): + """ + Surface graphics. + """ + + def display(self): + """ + Displays contour graphics. + """ + plotter.set_graphics(self) + + class show_edges(metaclass=PyLocaPropertyMeta): + """ + Show edges for surface. + """ + + value = True + + class surface_type(metaclass=PyLocaPropertyMeta): + """ + Specify surface type. + """ + + def availability(self, name): + if name == "plane_surface": + return self.surface_type() == "plane-surface" + if name == "iso_surface": + return self.surface_type() == "iso-surface" + return True + + class surface_type(metaclass=PyLocaPropertyMeta): + value = "iso-surface" + + @Attribute + def allowed_values(self): + return ["plane_surface", "iso_surface"] + + class plane_surface(metaclass=PyLocaPropertyMeta): + """ + Plane surface data. + """ + + class iso_surface(metaclass=PyLocaPropertyMeta): + """ + Iso surface data. + """ + + class field(metaclass=PyLocaPropertyMeta): + """ + Iso surface field. + """ + + @Attribute + def allowed_values(self): + return [ + v["solver_name"] + for k, v in FieldData( + self.session.field_service + ) + .get_fields_info() + .items() + ] + + class rendering(metaclass=PyLocaPropertyMeta): + """ + Iso surface rendering. + """ + + value = "mesh" + + @Attribute + def allowed_values(self): + return ["mesh", "contour"] + + class iso_value(metaclass=PyLocaPropertyMeta): + """ + Iso surface iso value. + """ + + def _reset_on_change(self): + return [self.parent.field] + + @property + def value(self): + if ( + not hasattr(self, "_value") + or self._value == None + ): + range = self.range + self._value = range[0] if range else None + return self._value + + @value.setter + def value(self, value): + self._value = value + + @Attribute + def range(self): + field = self.parent.field() + if field: + return FieldData( + self.session.field_service + ).get_range(field) + + +class contour(metaclass=PyLocalNamedObjectMeta): + """ + Contour graphics. + """ + + def display(self): + """ + Displays Contour graphics. + """ + plotter.set_graphics(self) + + class field(metaclass=PyLocaPropertyMeta): + """ + Contour field. + """ + + @Attribute + def allowed_values(self): + return [ + v["solver_name"] + for k, v in FieldData(self.session.field_service) + .get_fields_info() + .items() + ] + + class surfaces_list(metaclass=PyLocaPropertyMeta): + """ + Contour surfaces. + """ + + @Attribute + def allowed_values(self): + return list( + FieldData(self.session.field_service) + .get_surfaces_info() + .keys() + ) + + class filled(metaclass=PyLocaPropertyMeta): + """ + Show filled contour. + """ + + value = True + + class node_values(metaclass=PyLocaPropertyMeta): + """ + Show nodal data. + """ + + value = True + + class boundary_values(metaclass=PyLocaPropertyMeta): + """ + Show boundary values. + """ + + value = False + + class contour_lines(metaclass=PyLocaPropertyMeta): + """ + Show contour lines. + """ + + value = False + + class show_edges(metaclass=PyLocaPropertyMeta): + """ + Show edges. + """ + + value = False + + class range_option(metaclass=PyLocaPropertyMeta): + """ + Specify range options. + """ + + def availability(self, name): + if name == "auto_range_on": + return self.range_option() == "auto-range-on" + if name == "auto_range_off": + return self.range_option() == "auto-range-off" + return True + + class range_option(metaclass=PyLocaPropertyMeta): + __doc__ = "" + value = "auto-range-on" + + @Attribute + def allowed_values(self): + return ["auto-range-on", "auto-range-off"] + + class auto_range_on(metaclass=PyLocaPropertyMeta): + class global_range(metaclass=PyLocaPropertyMeta): + """ + Show global range. + """ + + value = False + + class auto_range_off(metaclass=PyLocaPropertyMeta): + __doc__ = "" + + class clip_to_range(metaclass=PyLocaPropertyMeta): + """ + Clip contour within range. + """ + + value = False + + class minimum(metaclass=PyLocaPropertyMeta): + """ + Range minimum. + """ + + def _reset_on_change(self): + return [ + self.parent.parent.parent.field, + self.parent.parent.parent.node_values, + ] + + @property + def value(self): + if ( + not hasattr(self, "_value") + or self._value == None + ): + field = self.parent.parent.parent.field() + if field: + field_range = FieldData( + self.session.field_service + ).get_range( + field, + self.parent.parent.parent.node_values(), + ) + self._value = field_range[0] + return self._value + + @value.setter + def value(self, value): + self._value = value + + class maximum(metaclass=PyLocaPropertyMeta): + """ + Range maximum. + """ + + def _reset_on_change(self): + return [ + self.parent.parent.parent.field, + self.parent.parent.parent.node_values, + ] + + @property + def value(self): + if ( + not hasattr(self, "_value") + or self._value == None + ): + field = self.parent.parent.parent.field() + if field: + field_range = FieldData( + self.session.field_service + ).get_range( + field, + self.parent.parent.parent.node_values(), + ) + self._value = field_range[1] + + return self._value + + @value.setter + def value(self, value): + self._value = value diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py new file mode 100644 index 00000000000..42abd3cd26c --- /dev/null +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -0,0 +1,356 @@ +import numpy as np +import pyvista as pv +from ansys.fluent.core.core import FieldData +from pyvistaqt import BackgroundPlotter +import threading, copy + + +class Singleton(type): + __instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls.__instances: + cls.__instances[cls] = super(Singleton, cls).__call__( + *args, **kwargs + ) + return cls.__instances[cls] + + +class _Plotter(metaclass=Singleton): + """ + Plots the graphics. + + Properties + ---------- + background_plotter + BackgroundPlotter to plot graphics + + Methods + ------- + set_graphics(obj) + sets the graphics to be plotted. + + close(obj) + closes the background_plotter. + """ + + __lock = threading.Lock() + + def __init__(self): + self.__exit = False + self.__background_plotter = None + self.__graphics = None + self.__plotted_graphics_properties = None + + @property + def background_plotter(self): + return self.__background_plotter + + def close(self) -> None: + with self.__lock: + self.__exit = True + + def set_graphics(self, obj: object) -> None: + background_plotter = None + with self.__lock: + self.__graphics = obj + background_plotter = self.__background_plotter + + if not background_plotter: + thread = threading.Thread(target=self._display, args=()) + thread.start() + + # private methods + def _init_properties(self): + self.__background_plotter.theme.cmap = "jet" + self.__background_plotter.background_color = "white" + self.__background_plotter.theme.font.color = "black" + + def _display(self): + self.__background_plotter = BackgroundPlotter(title="PyFluent") + self._init_properties() + self.refresh() + self.__background_plotter.add_callback(self.refresh, 100) + self.__background_plotter.app.exec_() + + def _display_contour(self, obj): + if not obj.surfaces_list() or not obj.field(): + return + + # contour properties + field = obj.field() + range_option = obj.range_option.range_option() + filled = obj.filled() + contour_lines = obj.contour_lines() + node_values = obj.node_values() + boundary_values = obj.boundary_values() + + # scalar bar properties + scalar_bar_args = dict( + title_font_size=20, + label_font_size=16, + shadow=True, + fmt="%.6e", + font_family="arial", + vertical=True, + position_x=0.06, + position_y=0.3, + ) + + field_data = FieldData(obj.session.field_service) + surfaces_info = field_data.get_surfaces_info() + surface_ids = [ + id + for surf in obj.surfaces_list() + for id in surfaces_info.get(surf, {}).get("surface_id", []) + ] + # get scalar field data + scalar_field_data = field_data.get_scalar_field( + surface_ids, + field, + node_values, + boundary_values, + ) + meta_data = None + plotter = self.__background_plotter + + # loop over all meshes + for mesh_data in scalar_field_data: + try: + + topology = ( + "line" if mesh_data["faces"][0][0] == 2 else "face" + ) + if topology == "line": + mesh = pv.PolyData( + np.array(mesh_data["vertices"]), + lines=np.hstack(mesh_data["faces"]), + ) + else: + mesh = pv.PolyData( + np.array(mesh_data["vertices"]), + faces=np.hstack(mesh_data["faces"]), + ) + if node_values: + mesh.point_data[field] = np.array( + mesh_data["scalar_field"] + ) + else: + mesh.cell_data[field] = np.array( + mesh_data["scalar_field"] + ) + if not meta_data: + meta_data = mesh_data["meta_data"] + + if range_option == "auto-range-off": + auto_range_off = obj.range_option.auto_range_off + if auto_range_off.clip_to_range(): + if ( + np.min(mesh[field]) + < auto_range_off.maximum() + ): + maximum_below = mesh.clip_scalar( + scalars=field, + value=auto_range_off.maximum(), + ) + if ( + np.max(maximum_below[field]) + > auto_range_off.minimum() + ): + minimum_above = ( + maximum_below.clip_scalar( + scalars=field, + invert=False, + value=auto_range_off.minimum(), + ) + ) + if filled: + plotter.add_mesh( + minimum_above, + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + + if (not filled or contour_lines) and ( + np.min(minimum_above[field]) + != np.max(minimum_above[field]) + ): + plotter.add_mesh( + minimum_above.contour( + isosurfaces=20 + ) + ) + else: + if filled: + plotter.add_mesh( + mesh, + clim=[ + auto_range_off.minimum(), + auto_range_off.maximum(), + ], + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + if (not filled or contour_lines) and ( + np.min(mesh[field]) != np.max(mesh[field]) + ): + plotter.add_mesh( + mesh.contour(isosurfaces=20) + ) + else: + auto_range_on = obj.range_option.auto_range_on + if auto_range_on.global_range(): + if filled: + plotter.add_mesh( + mesh, + clim=[ + meta_data.scalarFieldrange.globalmin, + meta_data.scalarFieldrange.globalmax, + ], + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + if (not filled or contour_lines) and ( + np.min(mesh[field]) != np.max(mesh[field]) + ): + plotter.add_mesh( + mesh.contour(isosurfaces=20) + ) + + else: + if filled: + plotter.add_mesh( + mesh, + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + if (not filled or contour_lines) and ( + np.min(mesh[field]) != np.max(mesh[field]) + ): + plotter.add_mesh( + mesh.contour(isosurfaces=20) + ) + except Exception as e: + print(e) + pass + + def _display_iso_surface(self, obj): + field = obj.surface_type.iso_surface.field() + if not field: + return + + surfaces_list = list( + FieldData(obj.session.field_service) + .get_surfaces_info() + .keys() + ) + iso_value = obj.surface_type.iso_surface.iso_value() + if "dummy" in surfaces_list: + obj.session.tui.surface.edit_surface( + "dummy", + obj.surface_type.iso_surface.field(), + "dummy", + (), + (), + obj.surface_type.iso_surface.iso_value(), + (), + ) + else: + obj.session.tui.surface.iso_surface( + field, "dummy", (), (), iso_value, () + ) + + from ansys.fluent.postprocessing.pyvista.graphics import ( + Graphics, + ) + + surfaces_list = list( + FieldData(obj.session.field_service) + .get_surfaces_info() + .keys() + ) + if not "dummy" in surfaces_list: + raise RuntimeError ( + f'Iso surface creation failed.' + ) + graphics_session = Graphics(obj.session) + if obj.surface_type.iso_surface.rendering() == "mesh": + mesh = graphics_session.mesh["dummy"] + mesh.surfaces_list = ["dummy"] + mesh.show_edges = True + self._display_mesh(mesh) + del graphics_session.mesh["dummy"] + else: + cont = graphics_session.contour["dummy"] + cont.field = obj.surface_type.iso_surface.field() + cont.surfaces_list = ["dummy"] + cont.show_edges = True + cont.range_option.auto_range_on.global_range = True + self._display_contour(cont) + del graphics_session.contour["dummy"] + obj.session.tui.surface.delete_surface("dummy") + + def _display_mesh(self, obj): + if not obj.surfaces_list(): + return + field_data = FieldData(obj.session.field_service) + surfaces_info = field_data.get_surfaces_info() + surface_ids = [ + id + for surf in obj.surfaces_list() + for id in surfaces_info.get(surf, {}).get("surface_id", []) + ] + surfaces_data = field_data.get_surfaces(surface_ids) + for mesh_data in surfaces_data: + topology = ( + "line" if mesh_data["faces"][0][0] == 2 else "face" + ) + if topology == "line": + mesh = pv.PolyData( + np.array(mesh_data["vertices"]), + lines=np.hstack(mesh_data["faces"]), + ) + else: + mesh = pv.PolyData( + np.array(mesh_data["vertices"]), + faces=np.hstack(mesh_data["faces"]), + ) + self.__background_plotter.add_mesh( + mesh, show_edges=obj.show_edges(), color="lightgrey" + ) + + def refresh(self): + with self.__lock: + obj = self.__graphics + if not obj: + return + if self.__exit: + self.__background_plotter.close() + return + if self.__plotted_graphics_properties == obj(): + return + + self.__plotted_graphics_properties = copy.deepcopy(obj()) + plotter = self.__background_plotter + plotter.clear() + + camera = plotter.camera.copy() + + if obj.__class__.__name__ == "mesh": + self._display_mesh(obj) + elif obj.__class__.__name__ == "surface": + if obj.surface_type.surface_type() == "iso-surface": + self._display_iso_surface(obj) + elif obj.__class__.__name__ == "contour": + self._display_contour(obj) + + + + plotter.camera = camera.copy() + + +plotter = _Plotter() diff --git a/ansys/fluent/session.py b/ansys/fluent/session.py index 78528c09d63..b2d42d34a7f 100644 --- a/ansys/fluent/session.py +++ b/ansys/fluent/session.py @@ -3,6 +3,8 @@ import threading from ansys.api.fluent.v0 import datamodel_pb2_grpc as DataModelGrpcModule +from ansys.api.fluent.v0 import fielddata_pb2_grpc as FieldGrpcModule +from ansys.fluent.core.core import FieldDataService from ansys.api.fluent.v0 import transcript_pb2 as TranscriptModule from ansys.api.fluent.v0 import transcript_pb2_grpc as TranscriptGrpcModule from ansys.api.fluent.v0 import health_pb2 as HealthModule @@ -16,6 +18,7 @@ def parse_server_info_file(filename: str): lines = f.readlines() return lines[0].strip(), lines[1].strip() + class Session: """ Encapsulates a Fluent connection. @@ -38,7 +41,7 @@ class Session: """ __all_sessions = [] - + __on_exit_cbs = [] def __init__(self, server_info_filepath): self.__is_exiting = False self.lock = threading.Lock() @@ -55,8 +58,10 @@ def __init__(self, server_info_filepath): self.transcript_thread.start() datamodel_stub = DataModelGrpcModule.DataModelStub(self.__channel) - self.service = DatamodelService(datamodel_stub, password) - self.tui = Session.Tui(self.service) + fielddata_stub = FieldGrpcModule.FieldDataStub(self.__channel) + self.datamodel_service = DatamodelService(datamodel_stub, password) + self.field_service = FieldDataService(fielddata_stub, password) + self.tui = Session.Tui(self.datamodel_service) health_stub = HealthGrpcModule.HealthStub(self.__channel) health_check_request = HealthModule.HealthCheckRequest() @@ -91,7 +96,7 @@ def health_check(self): return response_cls.ServingStatus.Name(response.status) else: return response_cls.ServingStatus.Name(response_cls.NOT_SERVING) - + def exit(self): """Close the Fluent connection and exit Fluent. @@ -105,16 +110,23 @@ def exit(self): self.__channel.close() self.__channel = None + def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.exit() + @classmethod + def register_on_exit(cls, callback): + cls.__on_exit_cbs.append(callback) + @staticmethod def exit_all(): for session in Session.__all_sessions: session.exit() + for cb in Session.__on_exit_cbs: + cb() class Tui: __application_modules = [] diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index bca0c000a97..7febe94514e 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -2,13 +2,33 @@ convert_path_to_grpc_path, PyMenu ) +from pprint import pformat +class Attribute: + VALID_NAMES = ["range", "allowed_values"] + def __init__(self, function): + self.function = function + + def __set_name__(self, obj, name): + if not name in self.VALID_NAMES: + raise ValueError( + f"Attribute {name} is not allowed. Expected values are {self.VALID_NAMES}" + ) + if not hasattr(obj, "attributes"): + obj.attributes = set() + obj.attributes.add(name) + + def __set__(self, obj, value): + raise AttributeError("Attributes are readonly.") + + def __get__(self, obj, type=None) -> object: + return self.function(obj) class PyMenuMeta(type): @classmethod def __create_init(cls): - def wrapper(self, path, service): + def wrapper(self, path, service): self.path = path self.service = service for name, cls in self.__class__.__dict__.items(): @@ -42,8 +62,9 @@ def wrapper(self): return PyMenu(self.service).get_child_names( convert_path_to_grpc_path(self.path)) return wrapper + - def __new__(cls, name, bases, attrs): + def __new__(cls, name, bases, attrs): attrs['__init__'] = cls.__create_init() attrs['__dir__'] = cls.__create_dir() if 'is_extended_tui' in attrs: @@ -53,12 +74,343 @@ def __new__(cls, name, bases, attrs): cls, name, bases, attrs) +class PyLocaPropertyMeta(type): + @classmethod + def __create_validate(cls): + def wrapper(self, value): + old_value = self() + if old_value and type(old_value) != type(value): + raise TypeError(f'Value {value} should be of type {type(old_value)}') + attrs = hasattr(self, "attributes") and getattr( + self, "attributes" + ) + if attrs: + for attr in attrs: + if attr == "range" and ( + value < self.range[0] or value > self.range[1] + ): + raise ValueError( + f'Value {value} is not within {self.range}.' + ) + if attr == "allowed_values": + if isinstance(value, list): + if not all( + v in self.allowed_values for v in value + ): + raise ValueError( + f'Values {value} are not within {self.allowed_values}.' + ) + elif not value in self.allowed_values: + raise ValueError( + f'Value {value} is not within {self.allowed_values}.' + ) + + return value + + return wrapper + + @classmethod + def __create_init(cls): + def wrapper(self, path, session, parent=None): + self._path = path + self.session = session + self.parent = parent + self._on_change_cbs = [] + reset_on_change = ( + hasattr(self, "_reset_on_change") + and getattr(self, "_reset_on_change")() + ) + if reset_on_change: + for obj in reset_on_change: + obj._register_on_change_cb( + lambda: setattr(self, "_value", None) + ) + for name, cls in self.__class__.__dict__.items(): + if cls.__class__.__name__ == "PyLocaPropertyMeta": + setattr( + self, + name, + cls(self._path + [(name, None)], session, self), + ) + if cls.__class__.__name__ == "PyLocalNamedObjectMeta": + setattr( + self, + name, + cls( + self._path + [(name, None)], + None, + session, + self, + ), + ) + + return wrapper + + @classmethod + def __create_getattribute(cls): + def wrapper(self, name): + if name == "availability": + return object.__getattribute__(self, "availability") + availability = ( + getattr(self, "availability")(name) + if hasattr(self, "availability") + else True + ) + if availability: + return object.__getattribute__(self, name) + else: + return None + + return wrapper + + @classmethod + def __create_get_state(cls): + def wrapper(self, show_attributes=False): + state = {} + for name, cls in self.__class__.__dict__.items(): + if cls.__class__.__name__ == "PyLocaPropertyMeta": + availability = ( + getattr(self, "availability")(name) + if hasattr(self, "availability") + else True + ) + if availability: + o = getattr(self, name) + state[name] = o(show_attributes) + attrs = ( + show_attributes + and hasattr(o, "attributes") + and getattr(o, "attributes") + ) + if attrs: + for attr in attrs: + state[name + "." + attr] = getattr( + o, attr + ) + + if len(state) > 0: + return state + else: + try: + return self.value + except: + return None + + return wrapper + + @classmethod + def __create_set_state(cls): + def wrapper(self, value): + if isinstance(value, dict): + for k, v in value.items(): + setattr(self, k, v) + else: + self.value = self._validate(value) + for on_change_cb in self._on_change_cbs: + on_change_cb() + + return wrapper + + @classmethod + def __create_setattr(cls): + def wrapper(self, name, value): + attr = hasattr(self, name) and getattr(self, name) + if ( + attr + and attr.__class__.__class__.__name__ + == "PyLocaPropertyMeta" + ): + attr.set_state(value) + else: + object.__setattr__(self, name, value) + + return wrapper + + @classmethod + def __create_register_on_change(cls): + def wrapper(self, on_change_cb): + self._on_change_cbs.append(on_change_cb) + + return wrapper + + @classmethod + def __create_repr(cls): + def wrapper(self): + data = self(True) + if isinstance(data, dict): + return pformat(data, depth=1, indent=2) + else: + return f"{data}" + + return wrapper + + def __new__(cls, name, bases, attrs): + attrs["__init__"] = cls.__create_init() + attrs["__call__"] = cls.__create_get_state() + attrs["__getattribute__"] = cls.__create_getattribute() + attrs["__setattr__"] = cls.__create_setattr() + attrs["__repr__"] = cls.__create_repr() + attrs["_validate"] = cls.__create_validate() + attrs[ + "_register_on_change_cb" + ] = cls.__create_register_on_change() + attrs["set_state"] = cls.__create_set_state() + attrs["parent"] = None + return super(PyLocaPropertyMeta, cls).__new__( + cls, name, bases, attrs + ) + + +class PyLocalNamedObjectMeta(type): + @classmethod + def __create_init(cls): + def wrapper(self, path, name, session, parent=None): + self._path = path[:-1] + [(path[-1][0], name)] + self.session = session + self.parent = parent + for name, cls in self.__class__.__dict__.items(): + if cls.__class__.__name__ == "PyLocaPropertyMeta": + setattr( + self, + name, + cls(self._path + [(name, None)], session, self), + ) + if cls.__class__.__name__ == "PyLocalNamedObjectMeta": + setattr( + self, + name, + cls( + self._path + [(name, None)], + None, + session, + self, + ), + ) + + return wrapper + + # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] + @classmethod + def __create_getitem(cls): + def wrapper(self, name): + o = self._collection.get(name, None) + if not o: + o = self._collection[name] = self.__class__( + self._path, name, self.session, self + ) + return o + + return wrapper + + # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] + # c2 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-2'] + # c1.update(c2()) + @classmethod + def __create_updateitem(cls): + def wrapper(self, value): + for name, val in value.items(): + getattr(self, name).set_state(val) + + return wrapper + + # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] + # ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-2'] = c1 + @classmethod + def __create_setitem(cls): + def wrapper(self, name, value): + o = self[name] + o.update(value()) + + return wrapper + + # del ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] + @classmethod + def __create_delitem(cls): + def wrapper(self, name): + del self._collection[name] + + return wrapper + + # ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1']() + @classmethod + def __create_get_state(cls): + def wrapper(self, show_attributes=False): + state = {} + for name, cls in self.__class__.__dict__.items(): + if cls.__class__.__name__ == "PyLocaPropertyMeta": + availability = ( + getattr(self, "availability")(name) + if hasattr(self, "availability") + else True + ) + if availability: + o = getattr(self, name) + state[name] = o(show_attributes) + attrs = ( + show_attributes + and hasattr(o, "attributes") + and getattr(o, "attributes") + ) + if attrs: + for attr in attrs: + state[name + "." + attr] = getattr( + o, attr + ) + return state + + return wrapper + + # ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'].field = "temperature" + @classmethod + def __create_setattr(cls): + def wrapper(self, name, value): + attr = hasattr(self, name) and getattr(self, name) + if ( + attr + and attr.__class__.__class__.__name__ + == "PyLocaPropertyMeta" + ): + getattr(self, name).set_state(value) + else: + object.__setattr__(self, name, value) + + return wrapper + + @classmethod + def __create_repr(cls): + def wrapper(self): + if self._path[-1][-1]: + return pformat(self(True), depth=1, indent=2) + else: + return object.__repr__(self) + + return wrapper + + def __new__(cls, name, bases, attrs): + attrs["_path"] = { + x: None for x in attrs["__qualname__"].split(".") + } + attrs["__init__"] = cls.__create_init() + attrs["__getitem__"] = cls.__create_getitem() + attrs["__setitem__"] = cls.__create_setitem() + attrs["__delitem__"] = cls.__create_delitem() + attrs["__call__"] = cls.__create_get_state() + attrs["__setattr__"] = cls.__create_setattr() + attrs["__repr__"] = cls.__create_repr() + attrs["_collection"] = {} + attrs["update"] = cls.__create_updateitem() + attrs["parent"] = None + return super(PyLocalNamedObjectMeta, cls).__new__( + cls, name, bases, attrs + ) + + class PyNamedObjectMeta(type): @classmethod def __create_init(cls): def wrapper(self, path, name, service): - self.path = path[:-1] + [(path[-1][0], name)] + self.path = path[:-1] + [(path[-1][0], name)] self.service = service for name, cls in self.__class__.__dict__.items(): if cls.__class__.__name__ == 'PyMenuMeta': diff --git a/ansys/fluent/solver/tui.py b/ansys/fluent/solver/tui.py index 9ae68ddc9d2..38b80b14fd0 100644 --- a/ansys/fluent/solver/tui.py +++ b/ansys/fluent/solver/tui.py @@ -3,7 +3,6 @@ from ansys.fluent.solver.meta import PyMenuMeta, PyNamedObjectMeta from ansys.fluent.core.core import PyMenu - def close_fluent(self, *args, **kwargs): """ Exit program. @@ -24,7 +23,8 @@ def print_license_usage(self, *args, **kwargs): Print license usage information """ return PyMenu(self.service).execute('/print_license_usage', *args, **kwargs) - + + class adjoint(metaclass=PyMenuMeta): __doc__ = 'Adjoint.' def observable(self, *args, **kwargs): @@ -413,7 +413,7 @@ class material_color(metaclass=PyMenuMeta): class display_state_name(metaclass=PyMenuMeta): __doc__ = '' is_extended_tui = True - + class contour(metaclass=PyNamedObjectMeta): __doc__ = '' is_extended_tui = True diff --git a/grpc/ansys-api-fluent-v0-0.0.1.tar.gz b/grpc/ansys-api-fluent-v0-0.0.1.tar.gz index 707acaca23e5b14be93005214a8ae97c57e56cb3..d2773d52f22da7053dd4695f8637e165210176c0 100644 GIT binary patch literal 20121 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 literal 8894 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 M1O7OLxB$2T05=niy#N3J From 91eb37070084c688f1b1798b5edcc6e2dac778be Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Tue, 25 Jan 2022 19:03:12 +0530 Subject: [PATCH 02/22] pyVista post processing --- .../fluent/postprocessing/pyvista/plotter.py | 12 +++---- ansys/fluent/solver/meta.py | 31 ++++++++++--------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 42abd3cd26c..0ad9e9b4907 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -267,16 +267,14 @@ def _display_iso_surface(self, obj): from ansys.fluent.postprocessing.pyvista.graphics import ( Graphics, ) - + surfaces_list = list( FieldData(obj.session.field_service) .get_surfaces_info() .keys() - ) + ) if not "dummy" in surfaces_list: - raise RuntimeError ( - f'Iso surface creation failed.' - ) + raise RuntimeError(f"Iso surface creation failed.") graphics_session = Graphics(obj.session) if obj.surface_type.iso_surface.rendering() == "mesh": mesh = graphics_session.mesh["dummy"] @@ -333,7 +331,7 @@ def refresh(self): return if self.__plotted_graphics_properties == obj(): return - + self.__plotted_graphics_properties = copy.deepcopy(obj()) plotter = self.__background_plotter plotter.clear() @@ -348,8 +346,6 @@ def refresh(self): elif obj.__class__.__name__ == "contour": self._display_contour(obj) - - plotter.camera = camera.copy() diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index d5edb1f2004..4f4b9060d95 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -1,7 +1,10 @@ from ansys.fluent.core.core import PyMenu, convert_path_to_grpc_path -from pprint import pformat +from pprint import pformat + + class Attribute: VALID_NAMES = ["range", "allowed_values"] + def __init__(self, function): self.function = function @@ -21,7 +24,6 @@ def __get__(self, obj, type=None) -> object: return self.function(obj) - class PyMenuMeta(type): @classmethod def __create_init(cls): @@ -80,14 +82,15 @@ def __new__(cls, name, bases, attrs): return super(PyMenuMeta, cls).__new__(cls, name, bases, attrs) - class PyLocaPropertyMeta(type): @classmethod def __create_validate(cls): def wrapper(self, value): old_value = self() if old_value and type(old_value) != type(value): - raise TypeError(f'Value {value} should be of type {type(old_value)}') + raise TypeError( + f"Value {value} should be of type {type(old_value)}" + ) attrs = hasattr(self, "attributes") and getattr( self, "attributes" ) @@ -97,20 +100,20 @@ def wrapper(self, value): value < self.range[0] or value > self.range[1] ): raise ValueError( - f'Value {value} is not within {self.range}.' - ) + f"Value {value} is not within {self.range}." + ) if attr == "allowed_values": if isinstance(value, list): if not all( v in self.allowed_values for v in value ): raise ValueError( - f'Values {value} are not within {self.allowed_values}.' - ) + f"Values {value} are not within {self.allowed_values}." + ) elif not value in self.allowed_values: raise ValueError( - f'Value {value} is not within {self.allowed_values}.' - ) + f"Value {value} is not within {self.allowed_values}." + ) return value @@ -262,7 +265,7 @@ def __new__(cls, name, bases, attrs): "_register_on_change_cb" ] = cls.__create_register_on_change() attrs["set_state"] = cls.__create_set_state() - attrs["parent"] = None + attrs["parent"] = None return super(PyLocaPropertyMeta, cls).__new__( cls, name, bases, attrs ) @@ -308,7 +311,7 @@ def wrapper(self, name): return o return wrapper - + # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] # c2 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-2'] # c1.update(c2()) @@ -387,7 +390,7 @@ def wrapper(self, name, value): def __create_repr(cls): def wrapper(self): if self._path[-1][-1]: - return pformat(self(True), depth=1, indent=2) + return pformat(self(True), depth=1, indent=2) else: return object.__repr__(self) @@ -404,7 +407,7 @@ def __new__(cls, name, bases, attrs): attrs["__call__"] = cls.__create_get_state() attrs["__setattr__"] = cls.__create_setattr() attrs["__repr__"] = cls.__create_repr() - attrs["_collection"] = {} + attrs["_collection"] = {} attrs["update"] = cls.__create_updateitem() attrs["parent"] = None return super(PyLocalNamedObjectMeta, cls).__new__( From 9dfc579d719d93065ee6c1daf13c93962f5e8781 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Tue, 25 Jan 2022 19:03:12 +0530 Subject: [PATCH 03/22] pyVista post processing --- README.rst | 42 +++++++++ ansys/fluent/core/core.py | 13 ++- .../fluent/postprocessing/pyvista/plotter.py | 12 +-- ansys/fluent/session.py | 6 +- ansys/fluent/solver/meta.py | 86 ++++++++++++------- ansys/fluent/solver/tui.py | 6 +- setup.py | 1 + 7 files changed, 112 insertions(+), 54 deletions(-) diff --git a/README.rst b/README.rst index 457540acef4..cba85dcbfef 100644 --- a/README.rst +++ b/README.rst @@ -31,6 +31,12 @@ Usage session.tui.define.models.unsteady_2nd_order("yes") session.tui.solve.initialize.initialize_flow() session.tui.solve.dual_time_iterate(number_of_time_steps=2, maximum_number_of_iterations_per_time_step=3) + +Post Processing +--------------- + +In Fluent(server) +----------------- session.tui.display.objects.contour['contour-1'] = {'boundary_values': True, 'color_map': {'color': 'field-velocity', 'font_automatic': True, 'font_name': 'Helvetica', 'font_size': 0.032, 'format': '%0.2e', 'length': 0.54, 'log_scale': False, 'position': 1, 'show_all': True, 'size': 100, 'user_skip': 9, 'visible': True, 'width': 6.0}, 'coloring': {'smooth': False}, 'contour_lines': False, 'display_state_name': 'None', 'draw_mesh': False, 'field': 'pressure', 'filled': True, 'mesh_object': '', 'node_values': True, 'range_option': {'auto_range_on': {'global_range': True}}, 'surfaces_list': [2, 5]} session.tui.display.objects.contour['contour-1']() session.tui.display.objects.contour['contour-1'].field.set_state('velocity-magnitude') @@ -39,5 +45,41 @@ Usage session.tui.display.objects.contour['contour-1'].color_map.size() session.tui.display.objects.contour['contour-1'].rename('my-contour') del session.tui.display.objects.contour['my-contour'] + +PyVista (client) +----------------- + #import module + import ansys.fluent.postprocessing.pyvista as pv + + #get the graphics objects for the session + + graphics_session1 = pv.Graphics(session) + mesh1 = graphics_session1.mesh["mesh-1"] + contour1 = graphics_session1.contour["contour-1"] + contour2 = graphics_session1.contour["contour-2"] + surface1 = graphics_session1.surface["surface-1"] + + #set graphics objects properties + + #mesh + mesh1.draw_mesh = True + mesh1.surfaces_list = ['symmetry'] + + #contour + contour1.field = "velocity-magnitude" + contour1.surfaces_list = ['symmetry'] + + contour2.field = "temperature" + contour2.surfaces_list = ['symmetry', 'wall'] + + #iso surface + surface1.surface_type.iso_surface.field= "velocity-magnitude" + surface1.surface_type.iso_surface.rendering= "contour" + + #display + contour1.display() + mesh1.display() + surface1.display() + session.exit() diff --git a/ansys/fluent/core/core.py b/ansys/fluent/core/core.py index b6ad427700b..df972c6ce2d 100644 --- a/ansys/fluent/core/core.py +++ b/ansys/fluent/core/core.py @@ -16,7 +16,7 @@ def convert_value_to_gvalue(val, gval): elif isinstance(val, str): gval.string_value = val elif isinstance(val, list) or isinstance(val, tuple): - # set the one_of to variant_vector_state + # set the one_of to variant_vector_state gval.list_value.values.add() gval.list_value.values.pop() for item in val: @@ -105,9 +105,9 @@ def execute_command(self, request): ) def execute_query(self, request): - return self.stub.ExecuteQuery(request, metadata=self.__get_metadata()) + return self.stub.ExecuteQuery(request, metadata=self.__get_metadata()) + - class FieldDataService: def __init__(self, stub, password: str): self.stub = stub @@ -133,8 +133,8 @@ def get_fields_info(self, request): def get_surfaces_info(self, request): return self.stub.GetSurfacesInfo( request, metadata=self.__get_metadata() - ) - + ) + def start_journal(filename: str): global JOURNAL_FILENAME JOURNAL_FILENAME = filename @@ -333,9 +333,6 @@ def get_scalar_field( return self._extract_scalar_field_data(response_iterator) - - - class PyMenu: class ExecuteCommandResult: def __init__(self, result): diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 42abd3cd26c..0ad9e9b4907 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -267,16 +267,14 @@ def _display_iso_surface(self, obj): from ansys.fluent.postprocessing.pyvista.graphics import ( Graphics, ) - + surfaces_list = list( FieldData(obj.session.field_service) .get_surfaces_info() .keys() - ) + ) if not "dummy" in surfaces_list: - raise RuntimeError ( - f'Iso surface creation failed.' - ) + raise RuntimeError(f"Iso surface creation failed.") graphics_session = Graphics(obj.session) if obj.surface_type.iso_surface.rendering() == "mesh": mesh = graphics_session.mesh["dummy"] @@ -333,7 +331,7 @@ def refresh(self): return if self.__plotted_graphics_properties == obj(): return - + self.__plotted_graphics_properties = copy.deepcopy(obj()) plotter = self.__background_plotter plotter.clear() @@ -348,8 +346,6 @@ def refresh(self): elif obj.__class__.__name__ == "contour": self._display_contour(obj) - - plotter.camera = camera.copy() diff --git a/ansys/fluent/session.py b/ansys/fluent/session.py index 085dd5252bd..a131b60c359 100644 --- a/ansys/fluent/session.py +++ b/ansys/fluent/session.py @@ -44,6 +44,7 @@ class Session: __all_sessions = [] __on_exit_cbs = [] + def __init__(self, server_info_filepath): self.__is_exiting = False self.lock = threading.Lock() @@ -98,7 +99,7 @@ def health_check(self): return response_cls.ServingStatus.Name(response.status) else: return response_cls.ServingStatus.Name(response_cls.NOT_SERVING) - + def exit(self): """Close the Fluent connection and exit Fluent.""" with self.lock: @@ -110,7 +111,6 @@ def exit(self): self.__channel.close() self.__channel = None - def __enter__(self): return self @@ -120,7 +120,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): @classmethod def register_on_exit(cls, callback): cls.__on_exit_cbs.append(callback) - + @staticmethod def exit_all(): for session in Session.__all_sessions: diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index d5edb1f2004..4c60c683d1e 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -1,14 +1,18 @@ from ansys.fluent.core.core import PyMenu, convert_path_to_grpc_path -from pprint import pformat +from pprint import pformat + + class Attribute: VALID_NAMES = ["range", "allowed_values"] + def __init__(self, function): self.function = function def __set_name__(self, obj, name): if not name in self.VALID_NAMES: raise ValueError( - f"Attribute {name} is not allowed. Expected values are {self.VALID_NAMES}" + f"Attribute {name} is not allowed."\ + f"Expected values are {self.VALID_NAMES}" ) if not hasattr(obj, "attributes"): obj.attributes = set() @@ -17,21 +21,22 @@ def __set_name__(self, obj, name): def __set__(self, obj, value): raise AttributeError("Attributes are readonly.") - def __get__(self, obj, type=None) -> object: + def __get__(self, obj, type=None): return self.function(obj) - class PyMenuMeta(type): @classmethod def __create_init(cls): - def wrapper(self, path, service): + def wrapper(self, path, service): self.path = path self.service = service for name, cls in self.__class__.__dict__.items(): if cls.__class__.__name__ == "PyMenuMeta": setattr( - self, name, cls(self.path + [(name, None)], service) + self, + name, + cls(self.path + [(name, None)], service), ) if cls.__class__.__name__ == "PyNamedObjectMeta": setattr( @@ -70,7 +75,7 @@ def wrapper(self): ) return wrapper - + def __new__(cls, name, bases, attrs): attrs["__init__"] = cls.__create_init() attrs["__dir__"] = cls.__create_dir() @@ -80,14 +85,15 @@ def __new__(cls, name, bases, attrs): return super(PyMenuMeta, cls).__new__(cls, name, bases, attrs) - class PyLocaPropertyMeta(type): @classmethod def __create_validate(cls): def wrapper(self, value): old_value = self() if old_value and type(old_value) != type(value): - raise TypeError(f'Value {value} should be of type {type(old_value)}') + raise TypeError( + f"Value {value} should be of type {type(old_value)}" + ) attrs = hasattr(self, "attributes") and getattr( self, "attributes" ) @@ -97,20 +103,22 @@ def wrapper(self, value): value < self.range[0] or value > self.range[1] ): raise ValueError( - f'Value {value} is not within {self.range}.' - ) + f"Value {value} is not within {self.range}." + ) if attr == "allowed_values": if isinstance(value, list): if not all( v in self.allowed_values for v in value ): raise ValueError( - f'Values {value} are not within {self.allowed_values}.' - ) + f"Values {value} are not within "\ + f"{self.allowed_values}." + ) elif not value in self.allowed_values: raise ValueError( - f'Value {value} is not within {self.allowed_values}.' - ) + f"Value {value} is not within "\ + f"{self.allowed_values}." + ) return value @@ -262,7 +270,7 @@ def __new__(cls, name, bases, attrs): "_register_on_change_cb" ] = cls.__create_register_on_change() attrs["set_state"] = cls.__create_set_state() - attrs["parent"] = None + attrs["parent"] = None return super(PyLocaPropertyMeta, cls).__new__( cls, name, bases, attrs ) @@ -296,7 +304,8 @@ def wrapper(self, path, name, session, parent=None): return wrapper - # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # c1 = graphics.contour['contour-1'] @classmethod def __create_getitem(cls): def wrapper(self, name): @@ -308,9 +317,10 @@ def wrapper(self, name): return o return wrapper - - # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] - # c2 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-2'] + + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # c1 = graphics.contour['contour-1'] + # c2 = graphics.contour['contour-2'] # c1.update(c2()) @classmethod def __create_updateitem(cls): @@ -320,8 +330,9 @@ def wrapper(self, value): return wrapper - # c1 = ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] - # ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-2'] = c1 + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # c1 = graphics.contour['contour-1'] + # graphics.contour['contour-2'] = c1 @classmethod def __create_setitem(cls): def wrapper(self, name, value): @@ -330,7 +341,8 @@ def wrapper(self, name, value): return wrapper - # del ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'] + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # del graphics.contour['contour-1'] @classmethod def __create_delitem(cls): def wrapper(self, name): @@ -338,7 +350,8 @@ def wrapper(self, name): return wrapper - # ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1']() + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # graphics.contour['contour-1']() @classmethod def __create_get_state(cls): def wrapper(self, show_attributes=False): @@ -367,7 +380,8 @@ def wrapper(self, show_attributes=False): return wrapper - # ansys.fluent.postprocessing.pyvista.Graphics(session1).contour['contour-1'].field = "temperature" + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # graphics.contour['contour-1'].field = "temperature" @classmethod def __create_setattr(cls): def wrapper(self, name, value): @@ -387,7 +401,7 @@ def wrapper(self, name, value): def __create_repr(cls): def wrapper(self): if self._path[-1][-1]: - return pformat(self(True), depth=1, indent=2) + return pformat(self(True), depth=1, indent=2) else: return object.__repr__(self) @@ -404,7 +418,7 @@ def __new__(cls, name, bases, attrs): attrs["__call__"] = cls.__create_get_state() attrs["__setattr__"] = cls.__create_setattr() attrs["__repr__"] = cls.__create_repr() - attrs["_collection"] = {} + attrs["_collection"] = {} attrs["update"] = cls.__create_updateitem() attrs["parent"] = None return super(PyLocalNamedObjectMeta, cls).__new__( @@ -416,12 +430,14 @@ class PyNamedObjectMeta(type): @classmethod def __create_init(cls): def wrapper(self, path, name, service): - self.path = path[:-1] + [(path[-1][0], name)] + self.path = path[:-1] + [(path[-1][0], name)] self.service = service for name, cls in self.__class__.__dict__.items(): if cls.__class__.__name__ == "PyMenuMeta": setattr( - self, name, cls(self.path + [(name, None)], service) + self, + name, + cls(self.path + [(name, None)], service), ) if cls.__class__.__name__ == "PyNamedObjectMeta": setattr( @@ -456,7 +472,9 @@ def wrapper(self, name, value): def __create_delitem(cls): def wrapper(self, name): o = self.__class__(self.path, name, self.service) - PyMenu(self.service).del_item(convert_path_to_grpc_path(o.path)) + PyMenu(self.service).del_item( + convert_path_to_grpc_path(o.path) + ) return wrapper @@ -481,11 +499,15 @@ def wrapper(self, new_name): return wrapper def __new__(cls, name, bases, attrs): - attrs["path"] = {x: None for x in attrs["__qualname__"].split(".")} + attrs["path"] = { + x: None for x in attrs["__qualname__"].split(".") + } attrs["__init__"] = cls.__create_init() attrs["__getitem__"] = cls.__create_getitem() attrs["__setitem__"] = cls.__create_setitem() attrs["__delitem__"] = cls.__create_delitem() attrs["__call__"] = cls.__create_get_state() attrs["rename"] = cls.__create_rename() - return super(PyNamedObjectMeta, cls).__new__(cls, name, bases, attrs) + return super(PyNamedObjectMeta, cls).__new__( + cls, name, bases, attrs + ) diff --git a/ansys/fluent/solver/tui.py b/ansys/fluent/solver/tui.py index 38b80b14fd0..9ae68ddc9d2 100644 --- a/ansys/fluent/solver/tui.py +++ b/ansys/fluent/solver/tui.py @@ -3,6 +3,7 @@ from ansys.fluent.solver.meta import PyMenuMeta, PyNamedObjectMeta from ansys.fluent.core.core import PyMenu + def close_fluent(self, *args, **kwargs): """ Exit program. @@ -23,8 +24,7 @@ def print_license_usage(self, *args, **kwargs): Print license usage information """ return PyMenu(self.service).execute('/print_license_usage', *args, **kwargs) - - + class adjoint(metaclass=PyMenuMeta): __doc__ = 'Adjoint.' def observable(self, *args, **kwargs): @@ -413,7 +413,7 @@ class material_color(metaclass=PyMenuMeta): class display_state_name(metaclass=PyMenuMeta): __doc__ = '' is_extended_tui = True - + class contour(metaclass=PyNamedObjectMeta): __doc__ = '' is_extended_tui = True diff --git a/setup.py b/setup.py index 732eb67dca1..107fe03278c 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ install_requires = [ "grpcio>=1.30.0", + "pyvista>=0.33.2" #'ansys-api-fluent-v0>=0.0.1' ] From 1f34fe31ee5ab49ae80a9b017a39fc9ecc685e6b Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Thu, 27 Jan 2022 15:17:09 +0530 Subject: [PATCH 04/22] pyVista Post processing --- README.rst | 16 +- ansys/fluent/core/core.py | 84 +++-- .../fluent/postprocessing/pyvista/graphics.py | 85 +++-- .../fluent/postprocessing/pyvista/plotter.py | 302 ++++++++---------- ansys/fluent/solver/meta.py | 27 +- 5 files changed, 250 insertions(+), 264 deletions(-) diff --git a/README.rst b/README.rst index cba85dcbfef..bcf412af88b 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Usage .. code:: python - import ansys.fluent.solver as pyfluent + import ansys.fluent.solver as pyfluent import logging pyfluent.setLogLevel(logging.DEBUG) # for development, by default only errors are shown session = pyfluent.launch_fluent() @@ -37,6 +37,9 @@ Post Processing In Fluent(server) ----------------- + +.. code:: python + session.tui.display.objects.contour['contour-1'] = {'boundary_values': True, 'color_map': {'color': 'field-velocity', 'font_automatic': True, 'font_name': 'Helvetica', 'font_size': 0.032, 'format': '%0.2e', 'length': 0.54, 'log_scale': False, 'position': 1, 'show_all': True, 'size': 100, 'user_skip': 9, 'visible': True, 'width': 6.0}, 'coloring': {'smooth': False}, 'contour_lines': False, 'display_state_name': 'None', 'draw_mesh': False, 'field': 'pressure', 'filled': True, 'mesh_object': '', 'node_values': True, 'range_option': {'auto_range_on': {'global_range': True}}, 'surfaces_list': [2, 5]} session.tui.display.objects.contour['contour-1']() session.tui.display.objects.contour['contour-1'].field.set_state('velocity-magnitude') @@ -48,16 +51,19 @@ In Fluent(server) PyVista (client) ----------------- + +.. code:: python + #import module import ansys.fluent.postprocessing.pyvista as pv #get the graphics objects for the session graphics_session1 = pv.Graphics(session) - mesh1 = graphics_session1.mesh["mesh-1"] - contour1 = graphics_session1.contour["contour-1"] - contour2 = graphics_session1.contour["contour-2"] - surface1 = graphics_session1.surface["surface-1"] + mesh1 = graphics_session1.Mesh["mesh-1"] + contour1 = graphics_session1.Contour["contour-1"] + contour2 = graphics_session1.Contour["contour-2"] + surface1 = graphics_session1.Surface["surface-1"] #set graphics objects properties diff --git a/ansys/fluent/core/core.py b/ansys/fluent/core/core.py index df972c6ce2d..aa0ddfe3733 100644 --- a/ansys/fluent/core/core.py +++ b/ansys/fluent/core/core.py @@ -3,6 +3,7 @@ from ansys.api.fluent.v0 import datamodel_pb2 as DataModelProtoModule from ansys.api.fluent.v0 import fielddata_pb2 as FieldDataProtoModule +from typing import List MODULE_NAME_ALIAS = "pyfluent" JOURNAL_FILENAME = None @@ -135,6 +136,7 @@ def get_surfaces_info(self, request): request, metadata=self.__get_metadata() ) + def start_journal(filename: str): global JOURNAL_FILENAME JOURNAL_FILENAME = filename @@ -225,26 +227,51 @@ def journal_global_fn_call(self, func_name, args=None, kwargs=None): self.__write_to_file(f"{k}={repr(v)}") self.__write_to_file(")\n") + class FieldData: - def __init__(self, service): - self.service = service + """ + Provide the field data. + + Methods + ------- + get_range(field: str, node_value: bool, surface_ids: List[int]) + -> List[float] + Get field range i.e. minimum and maximum value. + + get_fields_info(self) -> dict + Get fields information i.e. field name, domain and section. + + get_surfaces_info(self) -> dict + Get surfaces information i.e. surface name, id and type. - def get_range(self, field, node_value= False, surface_ids=[]): + get_surfaces(surface_ids: List[int], overset_mesh: bool) -> dict + Get surfaces data i.e. coordinates and connectivity. + + def get_scalar_field( + surface_ids: List[int], scalar_field: str, node_value: bool, + boundary_value: bool) -> dict + Get field data i.e. surface data and associated scalar field values. + + """ + + def __init__(self, service: FieldDataService): + self.__service = service + + def get_range( + self, field: str, node_value: bool = False, surface_ids: List[int] = [] + ) -> List[float]: request = FieldDataProtoModule.GetRangeRequest() request.fieldName = field request.nodeValue = node_value request.surfaceid.extend( - [ - FieldDataProtoModule.SurfaceId(id=int(id)) - for id in surface_ids - ] + [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): + 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, @@ -254,14 +281,12 @@ def get_fields_info(self): for field_info in response.fieldInfo } - def get_surfaces_info(self): + def get_surfaces_info(self) -> dict: request = FieldDataProtoModule.GetSurfacesInfoResponse() - response = self.service.get_surfaces_info(request) + response = self.__service.get_surfaces_info(request) return { surface_info.surfaceName: { - "surface_id": [ - surf.id for surf in surface_info.surfaceId - ], + "surface_id": [surf.id for surf in surface_info.surfaceId], "zone_id": surface_info.zoneId.id, "zone_type": surface_info.zoneType, "type": surface_info.type, @@ -284,16 +309,15 @@ def _extract_surfaces_data(self, response_iterator): for response in response_iterator ] - def get_surfaces(self, surface_ids, overset_mesh=False): + def get_surfaces( + self, surface_ids: List[int], overset_mesh: bool = False + ) -> dict: request = FieldDataProtoModule.GetSurfacesRequest() request.surfaceid.extend( - [ - FieldDataProtoModule.SurfaceId(id=int(id)) - for id in surface_ids - ] + [FieldDataProtoModule.SurfaceId(id=int(id)) for id in surface_ids] ) request.oversetMesh = overset_mesh - response_iterator = self.service.get_surfaces(request) + response_iterator = self.__service.get_surfaces(request) return self._extract_surfaces_data(response_iterator) def _extract_scalar_field_data(self, response_iterator): @@ -308,8 +332,7 @@ def _extract_scalar_field_data(self, response_iterator): for facet in response.scalarfielddata.surfacedata.facet ], "scalar_field": [ - data - for data in response.scalarfielddata.scalarfield.data + data for data in response.scalarfielddata.scalarfield.data ], "meta_data": response.scalarfielddata.scalarfieldmetadata, } @@ -317,19 +340,20 @@ def _extract_scalar_field_data(self, response_iterator): ] def get_scalar_field( - self, surface_ids, scalar_field, node_value, boundary_value - ): + self, + surface_ids: List[int], + scalar_field: str, + node_value: bool, + boundary_value: bool, + ) -> dict: request = FieldDataProtoModule.GetScalarFieldRequest() request.surfaceid.extend( - [ - FieldDataProtoModule.SurfaceId(id=int(id)) - for id in surface_ids - ] + [FieldDataProtoModule.SurfaceId(id=int(id)) for id in surface_ids] ) request.scalarfield = scalar_field request.nodevalue = node_value request.boundaryvalues = boundary_value - response_iterator = self.service.get_scalar_field(request) + response_iterator = self.__service.get_scalar_field(request) return self._extract_scalar_field_data(response_iterator) diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index ed6b4c302e1..1a9115c6208 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -1,5 +1,5 @@ from ansys.fluent.solver.meta import ( - PyLocaPropertyMeta, + PyLocalPropertyMeta, PyLocalNamedObjectMeta, Attribute, ) @@ -11,7 +11,7 @@ class Graphics: """ - Instantiate the graphics objects. + Graphics objects provider. """ def __init__(self, session): @@ -36,7 +36,7 @@ def _init_module(self, obj, mod): Session.register_on_exit(lambda: plotter.close()) -class mesh(metaclass=PyLocalNamedObjectMeta): +class Mesh(metaclass=PyLocalNamedObjectMeta): """ Mesh graphics. """ @@ -47,7 +47,7 @@ def display(self): """ plotter.set_graphics(self) - class surfaces_list(metaclass=PyLocaPropertyMeta): + class surfaces_list(metaclass=PyLocalPropertyMeta): """ List of surfaces for mesh graphics. """ @@ -60,7 +60,7 @@ def allowed_values(self): .keys() ) - class show_edges(metaclass=PyLocaPropertyMeta): + class show_edges(metaclass=PyLocalPropertyMeta): """ Show edges for mesh. """ @@ -68,7 +68,7 @@ class show_edges(metaclass=PyLocaPropertyMeta): value = False -class surface(metaclass=PyLocalNamedObjectMeta): +class Surface(metaclass=PyLocalNamedObjectMeta): """ Surface graphics. """ @@ -79,14 +79,14 @@ def display(self): """ plotter.set_graphics(self) - class show_edges(metaclass=PyLocaPropertyMeta): + class show_edges(metaclass=PyLocalPropertyMeta): """ Show edges for surface. """ value = True - class surface_type(metaclass=PyLocaPropertyMeta): + class surface_type(metaclass=PyLocalPropertyMeta): """ Specify surface type. """ @@ -98,24 +98,24 @@ def availability(self, name): return self.surface_type() == "iso-surface" return True - class surface_type(metaclass=PyLocaPropertyMeta): + class surface_type(metaclass=PyLocalPropertyMeta): value = "iso-surface" @Attribute def allowed_values(self): return ["plane_surface", "iso_surface"] - class plane_surface(metaclass=PyLocaPropertyMeta): + class plane_surface(metaclass=PyLocalPropertyMeta): """ Plane surface data. """ - class iso_surface(metaclass=PyLocaPropertyMeta): + class iso_surface(metaclass=PyLocalPropertyMeta): """ Iso surface data. """ - class field(metaclass=PyLocaPropertyMeta): + class field(metaclass=PyLocalPropertyMeta): """ Iso surface field. """ @@ -124,14 +124,12 @@ class field(metaclass=PyLocaPropertyMeta): def allowed_values(self): return [ v["solver_name"] - for k, v in FieldData( - self.session.field_service - ) + for k, v in FieldData(self.session.field_service) .get_fields_info() .items() ] - class rendering(metaclass=PyLocaPropertyMeta): + class rendering(metaclass=PyLocalPropertyMeta): """ Iso surface rendering. """ @@ -142,7 +140,7 @@ class rendering(metaclass=PyLocaPropertyMeta): def allowed_values(self): return ["mesh", "contour"] - class iso_value(metaclass=PyLocaPropertyMeta): + class iso_value(metaclass=PyLocalPropertyMeta): """ Iso surface iso value. """ @@ -152,10 +150,7 @@ def _reset_on_change(self): @property def value(self): - if ( - not hasattr(self, "_value") - or self._value == None - ): + if not hasattr(self, "_value") or self._value == None: range = self.range self._value = range[0] if range else None return self._value @@ -168,12 +163,12 @@ def value(self, value): def range(self): field = self.parent.field() if field: - return FieldData( - self.session.field_service - ).get_range(field) + return FieldData(self.session.field_service).get_range( + field + ) -class contour(metaclass=PyLocalNamedObjectMeta): +class Contour(metaclass=PyLocalNamedObjectMeta): """ Contour graphics. """ @@ -184,7 +179,7 @@ def display(self): """ plotter.set_graphics(self) - class field(metaclass=PyLocaPropertyMeta): + class field(metaclass=PyLocalPropertyMeta): """ Contour field. """ @@ -198,7 +193,7 @@ def allowed_values(self): .items() ] - class surfaces_list(metaclass=PyLocaPropertyMeta): + class surfaces_list(metaclass=PyLocalPropertyMeta): """ Contour surfaces. """ @@ -211,42 +206,42 @@ def allowed_values(self): .keys() ) - class filled(metaclass=PyLocaPropertyMeta): + class filled(metaclass=PyLocalPropertyMeta): """ Show filled contour. """ value = True - class node_values(metaclass=PyLocaPropertyMeta): + class node_values(metaclass=PyLocalPropertyMeta): """ Show nodal data. """ value = True - class boundary_values(metaclass=PyLocaPropertyMeta): + class boundary_values(metaclass=PyLocalPropertyMeta): """ Show boundary values. """ value = False - class contour_lines(metaclass=PyLocaPropertyMeta): + class contour_lines(metaclass=PyLocalPropertyMeta): """ Show contour lines. """ value = False - class show_edges(metaclass=PyLocaPropertyMeta): + class show_edges(metaclass=PyLocalPropertyMeta): """ Show edges. """ value = False - class range_option(metaclass=PyLocaPropertyMeta): + class range_option(metaclass=PyLocalPropertyMeta): """ Specify range options. """ @@ -258,7 +253,7 @@ def availability(self, name): return self.range_option() == "auto-range-off" return True - class range_option(metaclass=PyLocaPropertyMeta): + class range_option(metaclass=PyLocalPropertyMeta): __doc__ = "" value = "auto-range-on" @@ -266,25 +261,25 @@ class range_option(metaclass=PyLocaPropertyMeta): def allowed_values(self): return ["auto-range-on", "auto-range-off"] - class auto_range_on(metaclass=PyLocaPropertyMeta): - class global_range(metaclass=PyLocaPropertyMeta): + class auto_range_on(metaclass=PyLocalPropertyMeta): + class global_range(metaclass=PyLocalPropertyMeta): """ Show global range. """ value = False - class auto_range_off(metaclass=PyLocaPropertyMeta): + class auto_range_off(metaclass=PyLocalPropertyMeta): __doc__ = "" - class clip_to_range(metaclass=PyLocaPropertyMeta): + class clip_to_range(metaclass=PyLocalPropertyMeta): """ Clip contour within range. """ value = False - class minimum(metaclass=PyLocaPropertyMeta): + class minimum(metaclass=PyLocalPropertyMeta): """ Range minimum. """ @@ -297,10 +292,7 @@ def _reset_on_change(self): @property def value(self): - if ( - not hasattr(self, "_value") - or self._value == None - ): + if not hasattr(self, "_value") or self._value == None: field = self.parent.parent.parent.field() if field: field_range = FieldData( @@ -316,7 +308,7 @@ def value(self): def value(self, value): self._value = value - class maximum(metaclass=PyLocaPropertyMeta): + class maximum(metaclass=PyLocalPropertyMeta): """ Range maximum. """ @@ -329,10 +321,7 @@ def _reset_on_change(self): @property def value(self): - if ( - not hasattr(self, "_value") - or self._value == None - ): + if not hasattr(self, "_value") or self._value == None: field = self.parent.parent.parent.field() if field: field_range = FieldData( diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 0ad9e9b4907..49b1160eab2 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -2,7 +2,7 @@ import pyvista as pv from ansys.fluent.core.core import FieldData from pyvistaqt import BackgroundPlotter -import threading, copy +import threading class Singleton(type): @@ -34,32 +34,34 @@ class _Plotter(metaclass=Singleton): closes the background_plotter. """ - __lock = threading.Lock() + __condition = threading.Condition() def __init__(self): self.__exit = False self.__background_plotter = None self.__graphics = None - self.__plotted_graphics_properties = None @property def background_plotter(self): return self.__background_plotter def close(self) -> None: - with self.__lock: + with self.__condition: self.__exit = True def set_graphics(self, obj: object) -> None: - background_plotter = None - with self.__lock: + + with self.__condition: + plotter_initialized = self.__background_plotter self.__graphics = obj - background_plotter = self.__background_plotter - if not background_plotter: + if not plotter_initialized: thread = threading.Thread(target=self._display, args=()) thread.start() + with self.__condition: + self.__condition.wait() + # private methods def _init_properties(self): self.__background_plotter.theme.cmap = "jet" @@ -69,13 +71,13 @@ def _init_properties(self): def _display(self): self.__background_plotter = BackgroundPlotter(title="PyFluent") self._init_properties() - self.refresh() - self.__background_plotter.add_callback(self.refresh, 100) + self._refresh() + self.__background_plotter.add_callback(self._refresh, 100) self.__background_plotter.app.exec_() def _display_contour(self, obj): if not obj.surfaces_list() or not obj.field(): - return + raise RuntimeError("Contour definition is incomplete.") # contour properties field = obj.field() @@ -116,185 +118,151 @@ def _display_contour(self, obj): # loop over all meshes for mesh_data in scalar_field_data: - try: - topology = ( - "line" if mesh_data["faces"][0][0] == 2 else "face" + topology = "line" if mesh_data["faces"][0][0] == 2 else "face" + if topology == "line": + mesh = pv.PolyData( + np.array(mesh_data["vertices"]), + lines=np.hstack(mesh_data["faces"]), ) - if topology == "line": - mesh = pv.PolyData( - np.array(mesh_data["vertices"]), - lines=np.hstack(mesh_data["faces"]), - ) - else: - mesh = pv.PolyData( - np.array(mesh_data["vertices"]), - faces=np.hstack(mesh_data["faces"]), - ) - if node_values: - mesh.point_data[field] = np.array( - mesh_data["scalar_field"] - ) - else: - mesh.cell_data[field] = np.array( - mesh_data["scalar_field"] - ) - if not meta_data: - meta_data = mesh_data["meta_data"] - - if range_option == "auto-range-off": - auto_range_off = obj.range_option.auto_range_off - if auto_range_off.clip_to_range(): + else: + mesh = pv.PolyData( + np.array(mesh_data["vertices"]), + faces=np.hstack(mesh_data["faces"]), + ) + if node_values: + mesh.point_data[field] = np.array(mesh_data["scalar_field"]) + else: + mesh.cell_data[field] = np.array(mesh_data["scalar_field"]) + if not meta_data: + meta_data = mesh_data["meta_data"] + + if range_option == "auto-range-off": + auto_range_off = obj.range_option.auto_range_off + if auto_range_off.clip_to_range(): + if np.min(mesh[field]) < auto_range_off.maximum(): + maximum_below = mesh.clip_scalar( + scalars=field, + value=auto_range_off.maximum(), + ) if ( - np.min(mesh[field]) - < auto_range_off.maximum() + np.max(maximum_below[field]) + > auto_range_off.minimum() ): - maximum_below = mesh.clip_scalar( + minimum_above = maximum_below.clip_scalar( scalars=field, - value=auto_range_off.maximum(), + invert=False, + value=auto_range_off.minimum(), ) - if ( - np.max(maximum_below[field]) - > auto_range_off.minimum() + if filled: + plotter.add_mesh( + minimum_above, + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + + if (not filled or contour_lines) and ( + np.min(minimum_above[field]) + != np.max(minimum_above[field]) ): - minimum_above = ( - maximum_below.clip_scalar( - scalars=field, - invert=False, - value=auto_range_off.minimum(), - ) + plotter.add_mesh( + minimum_above.contour(isosurfaces=20) ) - if filled: - plotter.add_mesh( - minimum_above, - scalars=field, - show_edges=obj.show_edges(), - scalar_bar_args=scalar_bar_args, - ) - - if (not filled or contour_lines) and ( - np.min(minimum_above[field]) - != np.max(minimum_above[field]) - ): - plotter.add_mesh( - minimum_above.contour( - isosurfaces=20 - ) - ) - else: - if filled: - plotter.add_mesh( - mesh, - clim=[ - auto_range_off.minimum(), - auto_range_off.maximum(), - ], - scalars=field, - show_edges=obj.show_edges(), - scalar_bar_args=scalar_bar_args, - ) - if (not filled or contour_lines) and ( - np.min(mesh[field]) != np.max(mesh[field]) - ): - plotter.add_mesh( - mesh.contour(isosurfaces=20) - ) else: - auto_range_on = obj.range_option.auto_range_on - if auto_range_on.global_range(): - if filled: - plotter.add_mesh( - mesh, - clim=[ - meta_data.scalarFieldrange.globalmin, - meta_data.scalarFieldrange.globalmax, - ], - scalars=field, - show_edges=obj.show_edges(), - scalar_bar_args=scalar_bar_args, - ) - if (not filled or contour_lines) and ( - np.min(mesh[field]) != np.max(mesh[field]) - ): - plotter.add_mesh( - mesh.contour(isosurfaces=20) - ) + if filled: + plotter.add_mesh( + mesh, + clim=[ + auto_range_off.minimum(), + auto_range_off.maximum(), + ], + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + if (not filled or contour_lines) and ( + np.min(mesh[field]) != np.max(mesh[field]) + ): + plotter.add_mesh(mesh.contour(isosurfaces=20)) + else: + auto_range_on = obj.range_option.auto_range_on + if auto_range_on.global_range(): + if filled: + plotter.add_mesh( + mesh, + clim=[ + meta_data.scalarFieldrange.globalmin, + meta_data.scalarFieldrange.globalmax, + ], + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + if (not filled or contour_lines) and ( + np.min(mesh[field]) != np.max(mesh[field]) + ): + plotter.add_mesh(mesh.contour(isosurfaces=20)) - else: - if filled: - plotter.add_mesh( - mesh, - scalars=field, - show_edges=obj.show_edges(), - scalar_bar_args=scalar_bar_args, - ) - if (not filled or contour_lines) and ( - np.min(mesh[field]) != np.max(mesh[field]) - ): - plotter.add_mesh( - mesh.contour(isosurfaces=20) - ) - except Exception as e: - print(e) - pass + else: + if filled: + plotter.add_mesh( + mesh, + scalars=field, + show_edges=obj.show_edges(), + scalar_bar_args=scalar_bar_args, + ) + if (not filled or contour_lines) and ( + np.min(mesh[field]) != np.max(mesh[field]) + ): + plotter.add_mesh(mesh.contour(isosurfaces=20)) def _display_iso_surface(self, obj): field = obj.surface_type.iso_surface.field() if not field: - return + raise RuntimeError("Iso surface definition is incomplete.") + dummy_surface_name = "_dummy_iso_surface_for_pyfluent" surfaces_list = list( - FieldData(obj.session.field_service) - .get_surfaces_info() - .keys() + FieldData(obj.session.field_service).get_surfaces_info().keys() ) iso_value = obj.surface_type.iso_surface.iso_value() - if "dummy" in surfaces_list: - obj.session.tui.surface.edit_surface( - "dummy", - obj.surface_type.iso_surface.field(), - "dummy", - (), - (), - obj.surface_type.iso_surface.iso_value(), - (), - ) - else: - obj.session.tui.surface.iso_surface( - field, "dummy", (), (), iso_value, () - ) + if dummy_surface_name in surfaces_list: + obj.session.tui.surface.delete_surface(dummy_surface_name) + + obj.session.tui.surface.iso_surface( + field, dummy_surface_name, (), (), iso_value, () + ) from ansys.fluent.postprocessing.pyvista.graphics import ( Graphics, ) surfaces_list = list( - FieldData(obj.session.field_service) - .get_surfaces_info() - .keys() + FieldData(obj.session.field_service).get_surfaces_info().keys() ) - if not "dummy" in surfaces_list: - raise RuntimeError(f"Iso surface creation failed.") + if not dummy_surface_name in surfaces_list: + raise RuntimeError("Iso surface creation failed.") graphics_session = Graphics(obj.session) if obj.surface_type.iso_surface.rendering() == "mesh": - mesh = graphics_session.mesh["dummy"] - mesh.surfaces_list = ["dummy"] + mesh = graphics_session.Mesh[dummy_surface_name] + mesh.surfaces_list = [dummy_surface_name] mesh.show_edges = True self._display_mesh(mesh) - del graphics_session.mesh["dummy"] + del graphics_session.Mesh[dummy_surface_name] else: - cont = graphics_session.contour["dummy"] - cont.field = obj.surface_type.iso_surface.field() - cont.surfaces_list = ["dummy"] - cont.show_edges = True - cont.range_option.auto_range_on.global_range = True - self._display_contour(cont) - del graphics_session.contour["dummy"] - obj.session.tui.surface.delete_surface("dummy") + contour = graphics_session.Contour[dummy_surface_name] + contour.field = obj.surface_type.iso_surface.field() + contour.surfaces_list = [dummy_surface_name] + contour.show_edges = True + contour.range_option.auto_range_on.global_range = True + self._display_contour(contour) + del graphics_session.Contour[dummy_surface_name] + obj.session.tui.surface.delete_surface(dummy_surface_name) def _display_mesh(self, obj): if not obj.surfaces_list(): - return + raise RuntimeError("Mesh definition is incomplete.") field_data = FieldData(obj.session.field_service) surfaces_info = field_data.get_surfaces_info() surface_ids = [ @@ -304,9 +272,7 @@ def _display_mesh(self, obj): ] surfaces_data = field_data.get_surfaces(surface_ids) for mesh_data in surfaces_data: - topology = ( - "line" if mesh_data["faces"][0][0] == 2 else "face" - ) + topology = "line" if mesh_data["faces"][0][0] == 2 else "face" if topology == "line": mesh = pv.PolyData( np.array(mesh_data["vertices"]), @@ -321,32 +287,32 @@ def _display_mesh(self, obj): mesh, show_edges=obj.show_edges(), color="lightgrey" ) - def refresh(self): - with self.__lock: + def _refresh(self): + with self.__condition: obj = self.__graphics if not obj: + self.__condition.notify() return if self.__exit: self.__background_plotter.close() return - if self.__plotted_graphics_properties == obj(): - return - self.__plotted_graphics_properties = copy.deepcopy(obj()) + self.__graphics = None plotter = self.__background_plotter plotter.clear() camera = plotter.camera.copy() - if obj.__class__.__name__ == "mesh": + if obj.__class__.__name__ == "Mesh": self._display_mesh(obj) - elif obj.__class__.__name__ == "surface": + elif obj.__class__.__name__ == "Surface": if obj.surface_type.surface_type() == "iso-surface": self._display_iso_surface(obj) - elif obj.__class__.__name__ == "contour": + elif obj.__class__.__name__ == "Contour": self._display_contour(obj) plotter.camera = camera.copy() + self.__condition.notify() plotter = _Plotter() diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index 4c60c683d1e..942ecbaa0fe 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -85,7 +85,7 @@ def __new__(cls, name, bases, attrs): return super(PyMenuMeta, cls).__new__(cls, name, bases, attrs) -class PyLocaPropertyMeta(type): +class PyLocalPropertyMeta(type): @classmethod def __create_validate(cls): def wrapper(self, value): @@ -103,7 +103,7 @@ def wrapper(self, value): value < self.range[0] or value > self.range[1] ): raise ValueError( - f"Value {value} is not within {self.range}." + f"Value {value}, is not within {self.range}." ) if attr == "allowed_values": if isinstance(value, list): @@ -111,12 +111,13 @@ def wrapper(self, value): v in self.allowed_values for v in value ): raise ValueError( - f"Values {value} are not within "\ + f"Not all values in {value} are in the "\ + "list of allowed values, "\ f"{self.allowed_values}." ) elif not value in self.allowed_values: raise ValueError( - f"Value {value} is not within "\ + f"Value {value}, is not within "\ f"{self.allowed_values}." ) @@ -141,7 +142,7 @@ def wrapper(self, path, session, parent=None): lambda: setattr(self, "_value", None) ) for name, cls in self.__class__.__dict__.items(): - if cls.__class__.__name__ == "PyLocaPropertyMeta": + if cls.__class__.__name__ == "PyLocalPropertyMeta": setattr( self, name, @@ -183,7 +184,7 @@ def __create_get_state(cls): def wrapper(self, show_attributes=False): state = {} for name, cls in self.__class__.__dict__.items(): - if cls.__class__.__name__ == "PyLocaPropertyMeta": + if cls.__class__.__name__ == "PyLocalPropertyMeta": availability = ( getattr(self, "availability")(name) if hasattr(self, "availability") @@ -233,7 +234,7 @@ def wrapper(self, name, value): if ( attr and attr.__class__.__class__.__name__ - == "PyLocaPropertyMeta" + == "PyLocalPropertyMeta" ): attr.set_state(value) else: @@ -271,7 +272,7 @@ def __new__(cls, name, bases, attrs): ] = cls.__create_register_on_change() attrs["set_state"] = cls.__create_set_state() attrs["parent"] = None - return super(PyLocaPropertyMeta, cls).__new__( + return super(PyLocalPropertyMeta, cls).__new__( cls, name, bases, attrs ) @@ -284,7 +285,7 @@ def wrapper(self, path, name, session, parent=None): self.session = session self.parent = parent for name, cls in self.__class__.__dict__.items(): - if cls.__class__.__name__ == "PyLocaPropertyMeta": + if cls.__class__.__name__ == "PyLocalPropertyMeta": setattr( self, name, @@ -332,12 +333,12 @@ def wrapper(self, value): # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) # c1 = graphics.contour['contour-1'] - # graphics.contour['contour-2'] = c1 + # graphics.contour['contour-2'] = c1() @classmethod def __create_setitem(cls): def wrapper(self, name, value): o = self[name] - o.update(value()) + o.update(value) return wrapper @@ -357,7 +358,7 @@ def __create_get_state(cls): def wrapper(self, show_attributes=False): state = {} for name, cls in self.__class__.__dict__.items(): - if cls.__class__.__name__ == "PyLocaPropertyMeta": + if cls.__class__.__name__ == "PyLocalPropertyMeta": availability = ( getattr(self, "availability")(name) if hasattr(self, "availability") @@ -389,7 +390,7 @@ def wrapper(self, name, value): if ( attr and attr.__class__.__class__.__name__ - == "PyLocaPropertyMeta" + == "PyLocalPropertyMeta" ): getattr(self, name).set_state(value) else: From b7c9bb6fedbeefc8148d26744c625d10eb8e98ff Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Thu, 27 Jan 2022 17:19:29 +0530 Subject: [PATCH 05/22] pyVista post processing --- README.rst | 6 +++--- ansys/fluent/solver/meta.py | 16 ++++++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index bcf412af88b..8296f8c6e29 100644 --- a/README.rst +++ b/README.rst @@ -22,7 +22,7 @@ Usage .. code:: python - import ansys.fluent.solver as pyfluent + import ansys.fluent.solver as pyfluent import logging pyfluent.setLogLevel(logging.DEBUG) # for development, by default only errors are shown session = pyfluent.launch_fluent() @@ -35,8 +35,8 @@ Usage Post Processing --------------- -In Fluent(server) ------------------ +In Fluent (server) +------------------ .. code:: python diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index 942ecbaa0fe..6f28fc8817c 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -94,9 +94,7 @@ def wrapper(self, value): raise TypeError( f"Value {value} should be of type {type(old_value)}" ) - attrs = hasattr(self, "attributes") and getattr( - self, "attributes" - ) + attrs = getattr(self, "attributes", None) if attrs: for attr in attrs: if attr == "range" and ( @@ -195,8 +193,7 @@ def wrapper(self, show_attributes=False): state[name] = o(show_attributes) attrs = ( show_attributes - and hasattr(o, "attributes") - and getattr(o, "attributes") + and getattr(o, "attributes", False) ) if attrs: for attr in attrs: @@ -230,7 +227,7 @@ def wrapper(self, value): @classmethod def __create_setattr(cls): def wrapper(self, name, value): - attr = hasattr(self, name) and getattr(self, name) + attr = getattr(self, name, None) if ( attr and attr.__class__.__class__.__name__ @@ -369,8 +366,7 @@ def wrapper(self, show_attributes=False): state[name] = o(show_attributes) attrs = ( show_attributes - and hasattr(o, "attributes") - and getattr(o, "attributes") + and getattr(o, "attributes", None) ) if attrs: for attr in attrs: @@ -386,13 +382,13 @@ def wrapper(self, show_attributes=False): @classmethod def __create_setattr(cls): def wrapper(self, name, value): - attr = hasattr(self, name) and getattr(self, name) + attr = getattr(self, name, None) if ( attr and attr.__class__.__class__.__name__ == "PyLocalPropertyMeta" ): - getattr(self, name).set_state(value) + attr.set_state(value) else: object.__setattr__(self, name, value) From 929aa45bc06f9f804a86f81890fa0ff27ba7b015 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Thu, 27 Jan 2022 18:05:20 +0530 Subject: [PATCH 06/22] pyVista post processing --- ansys/fluent/postprocessing/pyvista/plotter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 49b1160eab2..6ce8ceebc1e 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -104,7 +104,7 @@ def _display_contour(self, obj): surface_ids = [ id for surf in obj.surfaces_list() - for id in surfaces_info.get(surf, {}).get("surface_id", []) + for id in surfaces_info[surf]["surface_id"] ] # get scalar field data scalar_field_data = field_data.get_scalar_field( From 14416e763f7889571d10eb1b9d42fe0924157f0a Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 28 Jan 2022 12:31:14 +0530 Subject: [PATCH 07/22] pyVista Post processing --- .../fluent/postprocessing/pyvista/graphics.py | 32 ++-- .../fluent/postprocessing/pyvista/plotter.py | 32 +++- ansys/fluent/services/field_data.py | 154 ++++++++++++++++++ 3 files changed, 198 insertions(+), 20 deletions(-) create mode 100644 ansys/fluent/services/field_data.py diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index 42df372071a..a8f47e5cead 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -1,11 +1,11 @@ +import sys +from ansys.fluent.postprocessing.pyvista.plotter import plotter +from ansys.fluent.session import Session from ansys.fluent.solver.meta import ( - PyLocalPropertyMeta, - PyLocalNamedObjectMeta, Attribute, + PyLocalNamedObjectMeta, + PyLocalPropertyMeta, ) -from ansys.fluent.postprocessing.pyvista.plotter import plotter -from ansys.fluent.session import Session -import sys class Graphics: @@ -53,7 +53,9 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): @Attribute def allowed_values(self): - return list(self.session.field_data.get_surfaces_info().keys()) + return list( + self.session.field_data.get_surfaces_info().keys() + ) class show_edges(metaclass=PyLocalPropertyMeta): """ @@ -144,7 +146,7 @@ def _reset_on_change(self): @property def value(self): - if not hasattr(self, "_value") or self._value == None: + if getattr(self, "_value", None) == None: range = self.range self._value = range[0] if range else None return self._value @@ -190,7 +192,9 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): @Attribute def allowed_values(self): - return list(self.session.field_data.get_surfaces_info().keys()) + return list( + self.session.field_data.get_surfaces_info().keys() + ) class filled(metaclass=PyLocalPropertyMeta): """ @@ -240,7 +244,7 @@ def availability(self, name): return True class range_option(metaclass=PyLocalPropertyMeta): - __doc__ = "" + value = "auto-range-on" @Attribute @@ -248,6 +252,9 @@ def allowed_values(self): return ["auto-range-on", "auto-range-off"] class auto_range_on(metaclass=PyLocalPropertyMeta): + """ + Specify auto range on. + """ class global_range(metaclass=PyLocalPropertyMeta): """ @@ -257,6 +264,9 @@ class global_range(metaclass=PyLocalPropertyMeta): value = False class auto_range_off(metaclass=PyLocalPropertyMeta): + """ + Specify auto range off. + """ class clip_to_range(metaclass=PyLocalPropertyMeta): """ @@ -278,7 +288,7 @@ def _reset_on_change(self): @property def value(self): - if not hasattr(self, "_value") or self._value == None: + if getattr(self, "_value", None) == None: field = self.parent.parent.parent.field() if field: field_range = self.session.field_data.get_range( @@ -305,7 +315,7 @@ def _reset_on_change(self): @property def value(self): - if not hasattr(self, "_value") or self._value == None: + if getattr(self, "_value", None) == None: field = self.parent.parent.parent.field() if field: field_range = self.session.field_data.get_range( diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index bbfe902defd..ccaeebcb7f4 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -1,7 +1,7 @@ +import threading import numpy as np -import pyvista as pv from pyvistaqt import BackgroundPlotter -import threading +import pyvista as pv class Singleton(type): @@ -118,7 +118,9 @@ def _display_contour(self, obj): # loop over all meshes for mesh_data in scalar_field_data: - topology = "line" if mesh_data["faces"][0][0] == 2 else "face" + topology = ( + "line" if mesh_data["faces"][0][0] == 2 else "face" + ) if topology == "line": mesh = pv.PolyData( np.array(mesh_data["vertices"]), @@ -130,9 +132,13 @@ def _display_contour(self, obj): faces=np.hstack(mesh_data["faces"]), ) if node_values: - mesh.point_data[field] = np.array(mesh_data["scalar_field"]) + mesh.point_data[field] = np.array( + mesh_data["scalar_field"] + ) else: - mesh.cell_data[field] = np.array(mesh_data["scalar_field"]) + mesh.cell_data[field] = np.array( + mesh_data["scalar_field"] + ) if not meta_data: meta_data = mesh_data["meta_data"] @@ -166,7 +172,9 @@ def _display_contour(self, obj): != np.max(minimum_above[field]) ): plotter.add_mesh( - minimum_above.contour(isosurfaces=20) + minimum_above.contour( + isosurfaces=20 + ) ) else: if filled: @@ -222,7 +230,9 @@ def _display_iso_surface(self, obj): raise RuntimeError("Iso surface definition is incomplete.") dummy_surface_name = "_dummy_iso_surface_for_pyfluent" - surfaces_list = list(obj.session.field_data.get_surfaces_info().keys()) + surfaces_list = list( + obj.session.field_data.get_surfaces_info().keys() + ) iso_value = obj.surface_type.iso_surface.iso_value() if dummy_surface_name in surfaces_list: obj.session.tui.surface.delete_surface(dummy_surface_name) @@ -235,7 +245,9 @@ def _display_iso_surface(self, obj): Graphics, ) - surfaces_list = list(obj.session.field_data.get_surfaces_info().keys()) + surfaces_list = list( + obj.session.field_data.get_surfaces_info().keys() + ) if not dummy_surface_name in surfaces_list: raise RuntimeError("Iso surface creation failed.") graphics_session = Graphics(obj.session) @@ -267,7 +279,9 @@ def _display_mesh(self, obj): ] surfaces_data = field_data.get_surfaces(surface_ids) for mesh_data in surfaces_data: - topology = "line" if mesh_data["faces"][0][0] == 2 else "face" + topology = ( + "line" if mesh_data["faces"][0][0] == 2 else "face" + ) if topology == "line": mesh = pv.PolyData( np.array(mesh_data["vertices"]), diff --git a/ansys/fluent/services/field_data.py b/ansys/fluent/services/field_data.py new file mode 100644 index 00000000000..156543258e8 --- /dev/null +++ b/ansys/fluent/services/field_data.py @@ -0,0 +1,154 @@ +from typing import List +import grpc +from ansys.api.fluent.v0 import fielddata_pb2 as FieldDataProtoModule +from ansys.api.fluent.v0 import fielddata_pb2_grpc as FieldGrpcModule + + +class FieldDataService: + def __init__(self, channel: grpc.Channel, metadata): + self.__stub = FieldGrpcModule.FieldDataStub(channel) + self.__metadata = metadata + + def get_surfaces(self, request): + return self.__stub.GetSurfaces(request, metadata=self.__metadata) + + def get_range(self, request): + return self.__stub.GetRange(request, metadata=self.__metadata) + + def get_scalar_field(self, request): + return self.__stub.GetScalarField(request, metadata=self.__metadata) + + def get_fields_info(self, request): + return self.__stub.GetFieldsInfo(request, metadata=self.__metadata) + + def get_surfaces_info(self, request): + return self.__stub.GetSurfacesInfo(request, metadata=self.__metadata) + + +class FieldData: + """ + Provide the field data. + + Methods + ------- + get_range(field: str, node_value: bool, surface_ids: List[int]) + -> List[float] + Get field range i.e. minimum and maximum value. + + get_fields_info(self) -> dict + Get fields information i.e. field name, domain and section. + + get_surfaces_info(self) -> dict + Get surfaces information i.e. surface name, id and type. + + get_surfaces(surface_ids: List[int], overset_mesh: bool) -> dict + Get surfaces data i.e. coordinates and connectivity. + + def get_scalar_field( + surface_ids: List[int], scalar_field: str, node_value: bool, + boundary_value: bool) -> dict + Get field data i.e. surface data and associated scalar field values. + + """ + + def __init__(self, service: FieldDataService): + self.__service = service + + def get_range( + self, field: str, node_value: bool = False, surface_ids: List[int] = [] + ) -> List[float]: + request = FieldDataProtoModule.GetRangeRequest() + request.fieldName = field + request.nodeValue = node_value + request.surfaceid.extend( + [FieldDataProtoModule.SurfaceId(id=int(id)) for id in surface_ids] + ) + 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) + return { + field_info.displayName: { + "solver_name": field_info.solverName, + "section": field_info.section, + "domain": field_info.domain, + } + for field_info in response.fieldInfo + } + + def get_surfaces_info(self) -> dict: + request = FieldDataProtoModule.GetSurfacesInfoResponse() + response = self.__service.get_surfaces_info(request) + return { + surface_info.surfaceName: { + "surface_id": [surf.id for surf in surface_info.surfaceId], + "zone_id": surface_info.zoneId.id, + "zone_type": surface_info.zoneType, + "type": surface_info.type, + } + for surface_info in response.surfaceInfo + } + + def _extract_surfaces_data(self, response_iterator): + return [ + { + "vertices": [ + [point.x, point.y, point.z] + for point in response.surfacedata.point + ], + "faces": [ + [len(facet.node)] + [node for node in facet.node] + for facet in response.surfacedata.facet + ], + } + for response in response_iterator + ] + + def get_surfaces( + self, surface_ids: List[int], overset_mesh: bool = False + ) -> dict: + request = FieldDataProtoModule.GetSurfacesRequest() + request.surfaceid.extend( + [FieldDataProtoModule.SurfaceId(id=int(id)) for id in surface_ids] + ) + request.oversetMesh = overset_mesh + response_iterator = self.__service.get_surfaces(request) + return self._extract_surfaces_data(response_iterator) + + def _extract_scalar_field_data(self, response_iterator): + return [ + { + "vertices": [ + [point.x, point.y, point.z] + for point in response.scalarfielddata.surfacedata.point + ], + "faces": [ + [len(facet.node)] + [node for node in facet.node] + for facet in response.scalarfielddata.surfacedata.facet + ], + "scalar_field": [ + data for data in response.scalarfielddata.scalarfield.data + ], + "meta_data": response.scalarfielddata.scalarfieldmetadata, + } + for response in response_iterator + ] + + def get_scalar_field( + self, + surface_ids: List[int], + scalar_field: str, + node_value: bool, + boundary_value: bool, + ) -> dict: + request = FieldDataProtoModule.GetScalarFieldRequest() + request.surfaceid.extend( + [FieldDataProtoModule.SurfaceId(id=int(id)) for id in surface_ids] + ) + request.scalarfield = scalar_field + request.nodevalue = node_value + request.boundaryvalues = boundary_value + response_iterator = self.__service.get_scalar_field(request) + return self._extract_scalar_field_data(response_iterator) From f8354cc2cf05a9128843aed5b8ada95cdd147f74 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 28 Jan 2022 13:31:26 +0530 Subject: [PATCH 08/22] pyVista Post processing --- ansys/fluent/core/core.py | 1 + ansys/fluent/postprocessing/pyvista/plotter.py | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ansys/fluent/core/core.py b/ansys/fluent/core/core.py index c60b716bf59..065f13c717f 100644 --- a/ansys/fluent/core/core.py +++ b/ansys/fluent/core/core.py @@ -3,6 +3,7 @@ MODULE_NAME_ALIAS = "pyfluent" JOURNAL_FILENAME = None + def start_journal(filename: str): global JOURNAL_FILENAME JOURNAL_FILENAME = filename diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index ccaeebcb7f4..f1407f9e404 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -17,20 +17,20 @@ def __call__(cls, *args, **kwargs): class _Plotter(metaclass=Singleton): """ - Plots the graphics. + Plot the graphics object. Properties ---------- background_plotter - BackgroundPlotter to plot graphics + BackgroundPlotter to plot graphics. Methods ------- set_graphics(obj) - sets the graphics to be plotted. + Set the graphics object to plot. close(obj) - closes the background_plotter. + Close the background_plotter. """ __condition = threading.Condition() From 02f3e26a4ac0f22c1da6caa861e740b93c493505 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 28 Jan 2022 14:41:04 +0530 Subject: [PATCH 09/22] pyVista Post processing --- .../fluent/postprocessing/pyvista/graphics.py | 8 +-- .../fluent/postprocessing/pyvista/plotter.py | 29 +++------ ansys/fluent/solver/meta.py | 63 +++++++------------ 3 files changed, 32 insertions(+), 68 deletions(-) diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index a8f47e5cead..f0733fb9697 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -53,9 +53,7 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): @Attribute def allowed_values(self): - return list( - self.session.field_data.get_surfaces_info().keys() - ) + return list(self.session.field_data.get_surfaces_info().keys()) class show_edges(metaclass=PyLocalPropertyMeta): """ @@ -192,9 +190,7 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): @Attribute def allowed_values(self): - return list( - self.session.field_data.get_surfaces_info().keys() - ) + return list(self.session.field_data.get_surfaces_info().keys()) class filled(metaclass=PyLocalPropertyMeta): """ diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index f1407f9e404..277b250f41b 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -31,6 +31,7 @@ class _Plotter(metaclass=Singleton): close(obj) Close the background_plotter. + """ __condition = threading.Condition() @@ -118,9 +119,7 @@ def _display_contour(self, obj): # loop over all meshes for mesh_data in scalar_field_data: - topology = ( - "line" if mesh_data["faces"][0][0] == 2 else "face" - ) + topology = "line" if mesh_data["faces"][0][0] == 2 else "face" if topology == "line": mesh = pv.PolyData( np.array(mesh_data["vertices"]), @@ -132,13 +131,9 @@ def _display_contour(self, obj): faces=np.hstack(mesh_data["faces"]), ) if node_values: - mesh.point_data[field] = np.array( - mesh_data["scalar_field"] - ) + mesh.point_data[field] = np.array(mesh_data["scalar_field"]) else: - mesh.cell_data[field] = np.array( - mesh_data["scalar_field"] - ) + mesh.cell_data[field] = np.array(mesh_data["scalar_field"]) if not meta_data: meta_data = mesh_data["meta_data"] @@ -172,9 +167,7 @@ def _display_contour(self, obj): != np.max(minimum_above[field]) ): plotter.add_mesh( - minimum_above.contour( - isosurfaces=20 - ) + minimum_above.contour(isosurfaces=20) ) else: if filled: @@ -230,9 +223,7 @@ def _display_iso_surface(self, obj): raise RuntimeError("Iso surface definition is incomplete.") dummy_surface_name = "_dummy_iso_surface_for_pyfluent" - surfaces_list = list( - obj.session.field_data.get_surfaces_info().keys() - ) + surfaces_list = list(obj.session.field_data.get_surfaces_info().keys()) iso_value = obj.surface_type.iso_surface.iso_value() if dummy_surface_name in surfaces_list: obj.session.tui.surface.delete_surface(dummy_surface_name) @@ -245,9 +236,7 @@ def _display_iso_surface(self, obj): Graphics, ) - surfaces_list = list( - obj.session.field_data.get_surfaces_info().keys() - ) + surfaces_list = list(obj.session.field_data.get_surfaces_info().keys()) if not dummy_surface_name in surfaces_list: raise RuntimeError("Iso surface creation failed.") graphics_session = Graphics(obj.session) @@ -279,9 +268,7 @@ def _display_mesh(self, obj): ] surfaces_data = field_data.get_surfaces(surface_ids) for mesh_data in surfaces_data: - topology = ( - "line" if mesh_data["faces"][0][0] == 2 else "face" - ) + topology = "line" if mesh_data["faces"][0][0] == 2 else "face" if topology == "line": mesh = pv.PolyData( np.array(mesh_data["vertices"]), diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index e74480cf995..55aff62765e 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -1,7 +1,7 @@ from ansys.fluent.services.tui_datamodel import ( PyMenu, - convert_path_to_grpc_path - ) + convert_path_to_grpc_path, +) from pprint import pformat @@ -14,7 +14,7 @@ def __init__(self, function): def __set_name__(self, obj, name): if not name in self.VALID_NAMES: raise ValueError( - f"Attribute {name} is not allowed."\ + f"Attribute {name} is not allowed." f"Expected values are {self.VALID_NAMES}" ) if not hasattr(obj, "attributes"): @@ -27,6 +27,7 @@ def __set__(self, obj, value): def __get__(self, obj, type=None): return self.function(obj) + class PyMenuMeta(type): @classmethod def __create_init(cls): @@ -103,7 +104,7 @@ def wrapper(self, value): value < self.range[0] or value > self.range[1] ): raise ValueError( - f"Value {value}, is not within valid range"\ + f"Value {value}, is not within valid range" f" {self.range}." ) if attr == "allowed_values": @@ -112,13 +113,13 @@ def wrapper(self, value): v in self.allowed_values for v in value ): raise ValueError( - f"Not all values in {value}, are in the "\ - "list of allowed values "\ + f"Not all values in {value}, are in the " + "list of allowed values " f"{self.allowed_values}." ) elif not value in self.allowed_values: raise ValueError( - f"Value {value}, is not in the list of "\ + f"Value {value}, is not in the list of " f"allowed values {self.allowed_values}." ) @@ -194,15 +195,12 @@ def wrapper(self, show_attributes=False): if availability: o = getattr(self, name) state[name] = o(show_attributes) - attrs = ( - show_attributes - and getattr(o, "attributes", False) + attrs = show_attributes and getattr( + o, "attributes", False ) if attrs: for attr in attrs: - state[name + "." + attr] = getattr( - o, attr - ) + state[name + "." + attr] = getattr(o, attr) if len(state) > 0: return state @@ -233,8 +231,7 @@ def wrapper(self, name, value): attr = getattr(self, name, None) if ( attr - and attr.__class__.__class__.__name__ - == "PyLocalPropertyMeta" + and attr.__class__.__class__.__name__ == "PyLocalPropertyMeta" ): attr.set_state(value) else: @@ -267,14 +264,10 @@ def __new__(cls, name, bases, attrs): attrs["__setattr__"] = cls.__create_setattr() attrs["__repr__"] = cls.__create_repr() attrs["_validate"] = cls.__create_validate() - attrs[ - "_register_on_change_cb" - ] = cls.__create_register_on_change() + attrs["_register_on_change_cb"] = cls.__create_register_on_change() attrs["set_state"] = cls.__create_set_state() attrs["parent"] = None - return super(PyLocalPropertyMeta, cls).__new__( - cls, name, bases, attrs - ) + return super(PyLocalPropertyMeta, cls).__new__(cls, name, bases, attrs) class PyLocalNamedObjectMeta(type): @@ -367,15 +360,12 @@ def wrapper(self, show_attributes=False): if availability: o = getattr(self, name) state[name] = o(show_attributes) - attrs = ( - show_attributes - and getattr(o, "attributes", None) + attrs = show_attributes and getattr( + o, "attributes", None ) if attrs: for attr in attrs: - state[name + "." + attr] = getattr( - o, attr - ) + state[name + "." + attr] = getattr(o, attr) return state return wrapper @@ -388,8 +378,7 @@ def wrapper(self, name, value): attr = getattr(self, name, None) if ( attr - and attr.__class__.__class__.__name__ - == "PyLocalPropertyMeta" + and attr.__class__.__class__.__name__ == "PyLocalPropertyMeta" ): attr.set_state(value) else: @@ -408,9 +397,7 @@ def wrapper(self): return wrapper def __new__(cls, name, bases, attrs): - attrs["_path"] = { - x: None for x in attrs["__qualname__"].split(".") - } + attrs["_path"] = {x: None for x in attrs["__qualname__"].split(".")} attrs["__init__"] = cls.__create_init() attrs["__getitem__"] = cls.__create_getitem() attrs["__setitem__"] = cls.__create_setitem() @@ -472,9 +459,7 @@ def wrapper(self, name, value): def __create_delitem(cls): def wrapper(self, name): o = self.__class__(self.path, name, self.service) - PyMenu(self.service).del_item( - convert_path_to_grpc_path(o.path) - ) + PyMenu(self.service).del_item(convert_path_to_grpc_path(o.path)) return wrapper @@ -499,15 +484,11 @@ def wrapper(self, new_name): return wrapper def __new__(cls, name, bases, attrs): - attrs["path"] = { - x: None for x in attrs["__qualname__"].split(".") - } + attrs["path"] = {x: None for x in attrs["__qualname__"].split(".")} attrs["__init__"] = cls.__create_init() attrs["__getitem__"] = cls.__create_getitem() attrs["__setitem__"] = cls.__create_setitem() attrs["__delitem__"] = cls.__create_delitem() attrs["__call__"] = cls.__create_get_state() attrs["rename"] = cls.__create_rename() - return super(PyNamedObjectMeta, cls).__new__( - cls, name, bases, attrs - ) + return super(PyNamedObjectMeta, cls).__new__(cls, name, bases, attrs) From 4d6e0607b28ddefad49f37d6a5dfdd66e54fd9f6 Mon Sep 17 00:00:00 2001 From: Mainak Kundu <94432368+mkundu1@users.noreply.github.com> Date: Fri, 28 Jan 2022 17:57:35 +0530 Subject: [PATCH 10/22] Install pyvista for python-3.10 (#25) --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++---------- Makefile | 6 ++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 587c1baae9f..b52680d4cf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: run: | make flake8 - testimport: + test-import: name: Smoke Tests runs-on: ${{ matrix.os }} strategy: @@ -66,17 +66,23 @@ jobs: restore-keys: | Python-${{ runner.os }}-${{ matrix.python-version }} + - name: Install pyvista for python 3.10 linux + if: ${{ runner.os == 'Linux' && matrix.python-version == '3.10' }} + run: make install-pyvista-for-python3.10-linux + + - name: Install pyvista for python 3.10 windows + if: ${{ runner.os == 'Windows' && matrix.python-version == '3.10' }} + run: make install-pyvista-for-python3.10-windows + - name: Install pyfluent - run: | - make install + run: make install - name: Test import - run: | - make test-import + run: make test-import build_test: name: Build and Unit Testing - needs: testimport + needs: test-import runs-on: ubuntu-latest steps: @@ -95,13 +101,19 @@ jobs: restore-keys: | Python-${{ runner.os }}-${{ matrix.python-version }} + - name: Install pyvista for python 3.10 linux + if: ${{ runner.os == 'Linux' && matrix.python-version == '3.10' }} + run: make install-pyvista-for-python3.10-linux + + - name: Install pyvista for python 3.10 windows + if: ${{ runner.os == 'Windows' && matrix.python-version == '3.10' }} + run: make install-pyvista-for-python3.10-windows + - name: Install pyfluent - run: | - make install + run: make install - name: Unit Testing - run: | - make unittest + run: make unittest - name: Check package run: | diff --git a/Makefile b/Makefile index 08fe6834305..daa0cb37554 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,12 @@ flake8: @echo "Running flake8" @flake8 . +install-pyvista-for-python3.10-linux: + @pip install https://github.com/pyvista/pyvista-wheels/raw/main/vtk-9.1.0.dev0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + +install-pyvista-for-python3.10-windows: + @pip install https://github.com/pyvista/pyvista-wheels/raw/main/vtk-9.1.0.dev0-cp310-cp310-win_amd64.whl + install: @pip install grpc/ansys-api-fluent-v0-0.0.1.tar.gz @pip install -r requirements_build.txt From ba9f6fca836943898e608a1f8b0bfe1cc31c9d81 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 28 Jan 2022 15:20:44 +0530 Subject: [PATCH 11/22] Corrected Iso surface range --- README.rst | 7 +++++-- ansys/fluent/postprocessing/pyvista/graphics.py | 2 +- ansys/fluent/postprocessing/pyvista/plotter.py | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 010dffd29a2..c43fed5f886 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,9 @@ For a local "development" version, install with: pip install -e . We need to install the grpc package as it is not yet in PyPI. +Also for python 3.10 we need to install `pyVista`_. + +.. _pyVista: https://github.com/pyvista/pyvista/discussions/2064 Usage ----- @@ -36,7 +39,7 @@ Post Processing --------------- In Fluent (server) ------------------- +^^^^^^^^^^^^^^^^^^ .. code:: python @@ -50,7 +53,7 @@ In Fluent (server) del session.tui.display.objects.contour['my-contour'] PyVista (client) ------------------ +^^^^^^^^^^^^^^^^^^ .. code:: python diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index f0733fb9697..413d50d5769 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -157,7 +157,7 @@ def value(self, value): def range(self): field = self.parent.field() if field: - return self.session.field_data.get_range(field) + return self.session.field_data.get_range(field, True) class Contour(metaclass=PyLocalNamedObjectMeta): diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 277b250f41b..1b9aa8bea03 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -264,7 +264,7 @@ def _display_mesh(self, obj): surface_ids = [ id for surf in obj.surfaces_list() - for id in surfaces_info.get(surf, {}).get("surface_id", []) + for id in surfaces_info[surf]["surface_id"] ] surfaces_data = field_data.get_surfaces(surface_ids) for mesh_data in surfaces_data: From 76da8ec5c2a218e192b2722d9c3e2df9b7a21708 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Mon, 31 Jan 2022 14:25:14 +0530 Subject: [PATCH 12/22] Container to store collection --- README.rst | 22 +++- .../fluent/postprocessing/pyvista/graphics.py | 17 +-- .../fluent/postprocessing/pyvista/plotter.py | 8 +- ansys/fluent/solver/meta.py | 115 ++++++++---------- 4 files changed, 81 insertions(+), 81 deletions(-) diff --git a/README.rst b/README.rst index c43fed5f886..44ab79ae0ff 100644 --- a/README.rst +++ b/README.rst @@ -63,10 +63,10 @@ PyVista (client) #get the graphics objects for the session graphics_session1 = pv.Graphics(session) - mesh1 = graphics_session1.Mesh["mesh-1"] - contour1 = graphics_session1.Contour["contour-1"] - contour2 = graphics_session1.Contour["contour-2"] - surface1 = graphics_session1.Surface["surface-1"] + mesh1 = graphics_session1.Meshes["mesh-1"] + contour1 = graphics_session1.Contours["contour-1"] + contour2 = graphics_session1.Contours["contour-2"] + surface1 = graphics_session1.Surfaces["surface-1"] #set graphics objects properties @@ -81,6 +81,20 @@ PyVista (client) contour2.field = "temperature" contour2.surfaces_list = ['symmetry', 'wall'] + #copy + graphics_session1.Contours["contour-3"] = contour2() + + #update + contour3 = graphics_session1.Contours["contour-3"] + contour3.update(contour1()) + + #delete + del graphics_session1.Contours["contour-3"] + + #loop + for name, _ in graphics_session1.Contours.items(): + print(name) + #iso surface surface1.surface_type.iso_surface.field= "velocity-magnitude" surface1.surface_type.iso_surface.rendering= "contour" diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index 413d50d5769..fd41ebacf78 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -5,6 +5,7 @@ Attribute, PyLocalNamedObjectMeta, PyLocalPropertyMeta, + PyLocalContainer, ) @@ -25,11 +26,7 @@ def _init_module(self, obj, mod): setattr(obj, name, cls_obj) self._init_module(cls_obj, module) if cls.__class__.__name__ == "PyLocalNamedObjectMeta": - setattr( - obj, - name, - cls([(name, None)], None, self.session, obj), - ) + setattr(obj, cls.PLURAL, PyLocalContainer(obj, cls)) Session.register_on_exit(lambda: plotter.close()) @@ -40,6 +37,8 @@ class Mesh(metaclass=PyLocalNamedObjectMeta): Mesh graphics. """ + PLURAL = "Meshes" + def display(self): """ Display mesh graphics. @@ -68,6 +67,8 @@ class Surface(metaclass=PyLocalNamedObjectMeta): Surface graphics. """ + PLURAL = "Surfaces" + def display(self): """ Display contour graphics. @@ -119,8 +120,8 @@ class field(metaclass=PyLocalPropertyMeta): def allowed_values(self): return [ v["solver_name"] - for k, v in self.session.field_data - .get_fields_info().items() + for k, v in self.session.field_data.get_fields_info() + .items() ] class rendering(metaclass=PyLocalPropertyMeta): @@ -165,6 +166,8 @@ class Contour(metaclass=PyLocalNamedObjectMeta): Contour graphics. """ + PLURAL = "Contours" + def display(self): """ Display Contour graphics. diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 1b9aa8bea03..f899a95fafb 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -241,19 +241,19 @@ def _display_iso_surface(self, obj): raise RuntimeError("Iso surface creation failed.") graphics_session = Graphics(obj.session) if obj.surface_type.iso_surface.rendering() == "mesh": - mesh = graphics_session.Mesh[dummy_surface_name] + mesh = graphics_session.Meshes[dummy_surface_name] mesh.surfaces_list = [dummy_surface_name] mesh.show_edges = True self._display_mesh(mesh) - del graphics_session.Mesh[dummy_surface_name] + del graphics_session.Meshes[dummy_surface_name] else: - contour = graphics_session.Contour[dummy_surface_name] + contour = graphics_session.Contours[dummy_surface_name] contour.field = obj.surface_type.iso_surface.field() contour.surfaces_list = [dummy_surface_name] contour.show_edges = True contour.range_option.auto_range_on.global_range = True self._display_contour(contour) - del graphics_session.Contour[dummy_surface_name] + del graphics_session.Contours[dummy_surface_name] obj.session.tui.surface.delete_surface(dummy_surface_name) def _display_mesh(self, obj): diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index 55aff62765e..fe8151df328 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -1,8 +1,9 @@ +from collections.abc import MutableMapping +from pprint import pformat from ansys.fluent.services.tui_datamodel import ( PyMenu, convert_path_to_grpc_path, ) -from pprint import pformat class Attribute: @@ -129,9 +130,8 @@ def wrapper(self, value): @classmethod def __create_init(cls): - def wrapper(self, path, session, parent=None): - self._path = path - self.session = session + def wrapper(self, parent): + self.session = parent.session self.parent = parent self._on_change_cbs = [] reset_on_change = ( @@ -148,18 +148,13 @@ def wrapper(self, path, session, parent=None): setattr( self, name, - cls(self._path + [(name, None)], session, self), + cls(self), ) if cls.__class__.__name__ == "PyLocalNamedObjectMeta": setattr( self, - name, - cls( - self._path + [(name, None)], - None, - session, - self, - ), + cls.PLURAL, + PyLocalContainer(self, cls), ) return wrapper @@ -273,45 +268,26 @@ def __new__(cls, name, bases, attrs): class PyLocalNamedObjectMeta(type): @classmethod def __create_init(cls): - def wrapper(self, path, name, session, parent=None): - self._path = path[:-1] + [(path[-1][0], name)] - self.session = session + def wrapper(self, name, parent): + self.__name = name + self.session = parent.session self.parent = parent for name, cls in self.__class__.__dict__.items(): if cls.__class__.__name__ == "PyLocalPropertyMeta": setattr( self, name, - cls(self._path + [(name, None)], session, self), + cls(self), ) if cls.__class__.__name__ == "PyLocalNamedObjectMeta": setattr( self, - name, - cls( - self._path + [(name, None)], - None, - session, - self, - ), + cls.PLURAL, + PyLocalContainer(self, cls), ) return wrapper - # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) - # c1 = graphics.contour['contour-1'] - @classmethod - def __create_getitem(cls): - def wrapper(self, name): - o = self._collection.get(name, None) - if not o: - o = self._collection[name] = self.__class__( - self._path, name, self.session, self - ) - return o - - return wrapper - # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) # c1 = graphics.contour['contour-1'] # c2 = graphics.contour['contour-2'] @@ -324,26 +300,6 @@ def wrapper(self, value): return wrapper - # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) - # c1 = graphics.contour['contour-1'] - # graphics.contour['contour-2'] = c1() - @classmethod - def __create_setitem(cls): - def wrapper(self, name, value): - o = self[name] - o.update(value) - - return wrapper - - # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) - # del graphics.contour['contour-1'] - @classmethod - def __create_delitem(cls): - def wrapper(self, name): - del self._collection[name] - - return wrapper - # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) # graphics.contour['contour-1']() @classmethod @@ -389,23 +345,15 @@ def wrapper(self, name, value): @classmethod def __create_repr(cls): def wrapper(self): - if self._path[-1][-1]: - return pformat(self(True), depth=1, indent=2) - else: - return object.__repr__(self) + return pformat(self(True), depth=1, indent=2) return wrapper def __new__(cls, name, bases, attrs): - attrs["_path"] = {x: None for x in attrs["__qualname__"].split(".")} attrs["__init__"] = cls.__create_init() - attrs["__getitem__"] = cls.__create_getitem() - attrs["__setitem__"] = cls.__create_setitem() - attrs["__delitem__"] = cls.__create_delitem() attrs["__call__"] = cls.__create_get_state() attrs["__setattr__"] = cls.__create_setattr() attrs["__repr__"] = cls.__create_repr() - attrs["_collection"] = {} attrs["update"] = cls.__create_updateitem() attrs["parent"] = None return super(PyLocalNamedObjectMeta, cls).__new__( @@ -413,6 +361,41 @@ def __new__(cls, name, bases, attrs): ) +class PyLocalContainer(MutableMapping): + def __init__(self, parent, object_class): + self.__object_class = object_class + self.__parent = parent + self.__collection: dict = {} + + def __iter__(self): + return iter(self.__collection) + + def __len__(self): + return len(self.__collection) + + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # c1 = graphics.Contours['contour-1'] + def __getitem__(self, name): + o = self.__collection.get(name, None) + if not o: + o = self.__collection[name] = self.__object_class( + name, self.__parent + ) + return o + + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # c1 = graphics.Contours['contour-1'] + # graphics.Contours['contour-2'] = c1() + def __setitem__(self, name, value): + o = self[name] + o.update(value) + + # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) + # del graphics.Contours['contour-1'] + def __delitem__(self, name): + del self.__collection[name] + + class PyNamedObjectMeta(type): @classmethod def __create_init(cls): From 87dae433fd8ed0bbba70fdc6e9a675e16619fa1c Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Mon, 31 Jan 2022 14:40:34 +0530 Subject: [PATCH 13/22] Container to store collection --- ansys/fluent/postprocessing/pyvista/graphics.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index fd41ebacf78..3dfa589c096 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -20,11 +20,6 @@ def __init__(self, session): def _init_module(self, obj, mod): for name, cls in mod.__dict__.items(): - if cls.__class__.__name__ == "module": - module = mod.__dict__[name] - cls_obj = type(name, (), {})() - setattr(obj, name, cls_obj) - self._init_module(cls_obj, module) if cls.__class__.__name__ == "PyLocalNamedObjectMeta": setattr(obj, cls.PLURAL, PyLocalContainer(obj, cls)) From de792607b8e63568c11990c826f4ae325f532bff Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Mon, 31 Jan 2022 16:05:07 +0530 Subject: [PATCH 14/22] Corrected readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 44ab79ae0ff..b880671fb32 100644 --- a/README.rst +++ b/README.rst @@ -71,7 +71,7 @@ PyVista (client) #set graphics objects properties #mesh - mesh1.draw_mesh = True + mesh1.show_edges = True mesh1.surfaces_list = ['symmetry'] #contour From c3494cb20e78d1bae5fed868f7e769bc78984251 Mon Sep 17 00:00:00 2001 From: Mainak Kundu <94432368+mkundu1@users.noreply.github.com> Date: Tue, 1 Feb 2022 11:34:26 +0530 Subject: [PATCH 15/22] Specify pyvista whl in setup.py (#35) --- .github/workflows/ci.yml | 16 ---------------- Makefile | 6 ------ README.rst | 30 ++++++++++++------------------ setup.py | 17 +++++++++++++++-- 4 files changed, 27 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b52680d4cf5..b15ef6e9f5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,14 +66,6 @@ jobs: restore-keys: | Python-${{ runner.os }}-${{ matrix.python-version }} - - name: Install pyvista for python 3.10 linux - if: ${{ runner.os == 'Linux' && matrix.python-version == '3.10' }} - run: make install-pyvista-for-python3.10-linux - - - name: Install pyvista for python 3.10 windows - if: ${{ runner.os == 'Windows' && matrix.python-version == '3.10' }} - run: make install-pyvista-for-python3.10-windows - - name: Install pyfluent run: make install @@ -101,14 +93,6 @@ jobs: restore-keys: | Python-${{ runner.os }}-${{ matrix.python-version }} - - name: Install pyvista for python 3.10 linux - if: ${{ runner.os == 'Linux' && matrix.python-version == '3.10' }} - run: make install-pyvista-for-python3.10-linux - - - name: Install pyvista for python 3.10 windows - if: ${{ runner.os == 'Windows' && matrix.python-version == '3.10' }} - run: make install-pyvista-for-python3.10-windows - - name: Install pyfluent run: make install diff --git a/Makefile b/Makefile index daa0cb37554..08fe6834305 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,6 @@ flake8: @echo "Running flake8" @flake8 . -install-pyvista-for-python3.10-linux: - @pip install https://github.com/pyvista/pyvista-wheels/raw/main/vtk-9.1.0.dev0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - -install-pyvista-for-python3.10-windows: - @pip install https://github.com/pyvista/pyvista-wheels/raw/main/vtk-9.1.0.dev0-cp310-cp310-win_amd64.whl - install: @pip install grpc/ansys-api-fluent-v0-0.0.1.tar.gz @pip install -r requirements_build.txt diff --git a/README.rst b/README.rst index b880671fb32..b55069f470c 100644 --- a/README.rst +++ b/README.rst @@ -10,14 +10,9 @@ For a local "development" version, install with: git clone https://github.com/pyansys/pyfluent.git cd pyfluent - pip install grpc\ansys-api-fluent-v0-0.0.1.tar.gz + pip install grpc/ansys-api-fluent-v0-0.0.1.tar.gz pip install -e . -We need to install the grpc package as it is not yet in PyPI. -Also for python 3.10 we need to install `pyVista`_. - -.. _pyVista: https://github.com/pyvista/pyvista/discussions/2064 - Usage ----- 1) Fluent should be installed from the latest daily build. PyFluent determines the Fluent launch path from AWP_ROOT222 environment variable. That environment variable can be modified to use a custom Fluent build. @@ -34,11 +29,11 @@ Usage session.tui.define.models.unsteady_2nd_order("yes") session.tui.solve.initialize.initialize_flow() session.tui.solve.dual_time_iterate(number_of_time_steps=2, maximum_number_of_iterations_per_time_step=3) - + Post Processing --------------- -In Fluent (server) +In Fluent (server) ^^^^^^^^^^^^^^^^^^ .. code:: python @@ -51,9 +46,9 @@ In Fluent (server) session.tui.display.objects.contour['contour-1'].color_map.size() session.tui.display.objects.contour['contour-1'].rename('my-contour') del session.tui.display.objects.contour['my-contour'] - -PyVista (client) -^^^^^^^^^^^^^^^^^^ + +PyVista (client) +^^^^^^^^^^^^^^^^ .. code:: python @@ -61,7 +56,7 @@ PyVista (client) import ansys.fluent.postprocessing.pyvista as pv #get the graphics objects for the session - + graphics_session1 = pv.Graphics(session) mesh1 = graphics_session1.Meshes["mesh-1"] contour1 = graphics_session1.Contours["contour-1"] @@ -69,7 +64,7 @@ PyVista (client) surface1 = graphics_session1.Surfaces["surface-1"] #set graphics objects properties - + #mesh mesh1.show_edges = True mesh1.surfaces_list = ['symmetry'] @@ -83,14 +78,14 @@ PyVista (client) #copy graphics_session1.Contours["contour-3"] = contour2() - + #update contour3 = graphics_session1.Contours["contour-3"] contour3.update(contour1()) - + #delete del graphics_session1.Contours["contour-3"] - + #loop for name, _ in graphics_session1.Contours.items(): print(name) @@ -103,6 +98,5 @@ PyVista (client) contour1.display() mesh1.display() surface1.display() - - session.exit() + session.exit() diff --git a/setup.py b/setup.py index fdaf303c03e..014d40ffedf 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,8 @@ """Setup file for ansys-fluent-solver""" import os +import platform +import struct +import sys from setuptools import find_namespace_packages, setup @@ -15,10 +18,20 @@ install_requires = [ "grpcio>=1.30.0", - "pyvista>=0.33.2" - #'ansys-api-fluent-v0>=0.0.1' + "pyvista>=0.33.2", ] +is64 = struct.calcsize("P") * 8 == 64 +if sys.version_info.minor == 10 and is64: + if platform.system().lower() == "linux": + install_requires.append( + "vtk @ https://github.com/pyvista/pyvista-wheels/raw/main/vtk-9.1.0.dev0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" # noqa: E501 + ) + elif platform.system().lower() == "windows": + install_requires.append( + "vtk @ https://github.com/pyvista/pyvista-wheels/raw/main/vtk-9.1.0.dev0-cp310-cp310-win_amd64.whl" # noqa: E501 + ) + packages = [] for package in find_namespace_packages(include="ansys*"): if package.startswith("ansys.fluent"): From e6132399fc16a1c40154edc16f29199e387101d1 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 12:47:34 +0530 Subject: [PATCH 16/22] pyVista post processing --- README.rst | 3 +-- ansys/fluent/postprocessing/pyvista/__init__.py | 4 ++-- ansys/fluent/postprocessing/pyvista/plotter.py | 6 +++--- ansys/fluent/solver/meta.py | 1 + 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 6f5c19c17cf..303cb9eeb3d 100644 --- a/README.rst +++ b/README.rst @@ -25,8 +25,7 @@ Usage session.check_health() session.tui.solver.file.read_case(case_file_name='elbow.cas.gz') session.tui.solver.define.models.unsteady_2nd_order("yes") - session.tui.solver.solve.initialize.initialize_flow() - session.tui.solve.dual_time_iterate(number_of_time_steps=2, maximum_number_of_iterations_per_time_step=3) + session.tui.solver.solve.initialize.initialize_flow() session.tui.solver.solve.dual_time_iterate(number_of_time_steps=2, maximum_number_of_iterations_per_time_step=3) Settings objects diff --git a/ansys/fluent/postprocessing/pyvista/__init__.py b/ansys/fluent/postprocessing/pyvista/__init__.py index 0d7d20b92b8..c1552773302 100644 --- a/ansys/fluent/postprocessing/pyvista/__init__.py +++ b/ansys/fluent/postprocessing/pyvista/__init__.py @@ -1,2 +1,2 @@ -from ansys.fluent.postprocessing.pyvista.plotter import plotter -from ansys.fluent.postprocessing.pyvista.graphics import Graphics +from ansys.fluent.postprocessing.pyvista.plotter import plotter # noqa: F401 +from ansys.fluent.postprocessing.pyvista.graphics import Graphics # noqa: F401 diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index f899a95fafb..d5c59da1483 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -226,9 +226,9 @@ def _display_iso_surface(self, obj): surfaces_list = list(obj.session.field_data.get_surfaces_info().keys()) iso_value = obj.surface_type.iso_surface.iso_value() if dummy_surface_name in surfaces_list: - obj.session.tui.surface.delete_surface(dummy_surface_name) + obj.session.tui.solver.surface.delete_surface(dummy_surface_name) - obj.session.tui.surface.iso_surface( + obj.session.tui.solver.surface.iso_surface( field, dummy_surface_name, (), (), iso_value, () ) @@ -254,7 +254,7 @@ def _display_iso_surface(self, obj): contour.range_option.auto_range_on.global_range = True self._display_contour(contour) del graphics_session.Contours[dummy_surface_name] - obj.session.tui.surface.delete_surface(dummy_surface_name) + obj.session.tui.solver.surface.delete_surface(dummy_surface_name) def _display_mesh(self, obj): if not obj.surfaces_list(): diff --git a/ansys/fluent/solver/meta.py b/ansys/fluent/solver/meta.py index ea9123020c5..dd077b05a03 100644 --- a/ansys/fluent/solver/meta.py +++ b/ansys/fluent/solver/meta.py @@ -1,5 +1,6 @@ from collections.abc import MutableMapping from pprint import pformat + # pylint: disable=unused-private-member # pylint: disable=bad-mcs-classmethod-argument from ansys.fluent.services.datamodel_tui import ( From 9ab895dccfbda1af02c32c9105e5d58bdd7029ac Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 13:34:04 +0530 Subject: [PATCH 17/22] PyVista Postprocessing --- .flake8 | 2 ++ README.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 6fae6bf9b61..b33711a789b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,7 @@ [flake8] exclude = venv, tui.py, ansys/api/fluent +per-file-ignores = + ansys/fluent/postprocessing/pyvista/graphics.py:N801 select = W191, W291, W293, W391, E115, E117, E122, E124, E125, E225, E231, E301, E303, E501, F401, F403, N801, N802, N803, N804, N805, N806 count = True max-complexity = 10 diff --git a/README.rst b/README.rst index 303cb9eeb3d..0c88d6a4cbf 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ TUI and meshing workflows from Fluent meshing are exposed. Please check `meshing Post Processing ---------------- +*************** In Fluent (server) ^^^^^^^^^^^^^^^^^^ From ca6698ae1aca0758cc8670cf37b5ff85d37d219d Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 14:03:07 +0530 Subject: [PATCH 18/22] Multi plotters support --- README.rst | 5 +- .../fluent/postprocessing/pyvista/graphics.py | 23 +-- .../fluent/postprocessing/pyvista/plotter.py | 143 +++++++++++------- ansys/fluent/session.py | 8 +- 4 files changed, 112 insertions(+), 67 deletions(-) diff --git a/README.rst b/README.rst index 0c88d6a4cbf..571f04adb44 100644 --- a/README.rst +++ b/README.rst @@ -113,9 +113,12 @@ PyVista (client) surface1.surface_type.iso_surface.field= "velocity-magnitude" surface1.surface_type.iso_surface.rendering= "contour" - #display + #display in default plotter contour1.display() mesh1.display() surface1.display() + + #display in seprate plotter e.g. plotter-2 + contour1.display("plotter-2") session.exit() \ No newline at end of file diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index 3dfa589c096..89820f6c5de 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -1,6 +1,6 @@ import sys +from typing import Optional from ansys.fluent.postprocessing.pyvista.plotter import plotter -from ansys.fluent.session import Session from ansys.fluent.solver.meta import ( Attribute, PyLocalNamedObjectMeta, @@ -24,9 +24,6 @@ def _init_module(self, obj, mod): setattr(obj, cls.PLURAL, PyLocalContainer(obj, cls)) -Session.register_on_exit(lambda: plotter.close()) - - class Mesh(metaclass=PyLocalNamedObjectMeta): """ Mesh graphics. @@ -34,11 +31,13 @@ class Mesh(metaclass=PyLocalNamedObjectMeta): PLURAL = "Meshes" - def display(self): + def display(self, plotter_id: Optional[str] = None): """ Display mesh graphics. """ - plotter.set_graphics(self) + plotter.set_graphics( + self, plotter_id if plotter_id else self.session.id + ) class surfaces_list(metaclass=PyLocalPropertyMeta): """ @@ -64,11 +63,13 @@ class Surface(metaclass=PyLocalNamedObjectMeta): PLURAL = "Surfaces" - def display(self): + def display(self, plotter_id: Optional[str] = None): """ Display contour graphics. """ - plotter.set_graphics(self) + plotter.set_graphics( + self, plotter_id if plotter_id else self.session.id + ) class show_edges(metaclass=PyLocalPropertyMeta): """ @@ -163,11 +164,13 @@ class Contour(metaclass=PyLocalNamedObjectMeta): PLURAL = "Contours" - def display(self): + def display(self, plotter_id: Optional[str] = None): """ Display Contour graphics. """ - plotter.set_graphics(self) + plotter.set_graphics( + self, plotter_id if plotter_id else self.session.id + ) class field(metaclass=PyLocalPropertyMeta): """ diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index d5c59da1483..845ca65d2bf 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -1,4 +1,6 @@ import threading +import sys +import signal import numpy as np from pyvistaqt import BackgroundPlotter import pyvista as pv @@ -28,52 +30,75 @@ class _Plotter(metaclass=Singleton): ------- set_graphics(obj) Set the graphics object to plot. - - close(obj) - Close the background_plotter. - """ __condition = threading.Condition() def __init__(self): self.__exit = False - self.__background_plotter = None - self.__graphics = None - - @property - def background_plotter(self): - return self.__background_plotter - - def close(self) -> None: - with self.__condition: - self.__exit = True + self.__active_plotter = None + self.__graphics = {} + self.__plotter_thread = None + self.__plotters = {} - def set_graphics(self, obj: object) -> None: + def set_graphics(self, obj: object, plotter_id: str) -> None: + if self.__exit: + return with self.__condition: - plotter_initialized = self.__background_plotter - self.__graphics = obj + self.__graphics[plotter_id] = obj + self.__active_plotter = self.__plotters.get(plotter_id) - if not plotter_initialized: - thread = threading.Thread(target=self._display, args=()) - thread.start() + if not self.__plotter_thread: + self.__plotter_thread = threading.Thread( + target=self._display, args=(), daemon=True + ) + self.__plotter_thread.start() with self.__condition: self.__condition.wait() + self.__plotters[plotter_id] = self.__active_plotter # private methods + + def _exit(self) -> None: + if self.__plotter_thread: + with self.__condition: + self.__exit = True + self.__condition.wait() + self.__plotter_thread.join() + self.__plotter_thread = None + def _init_properties(self): - self.__background_plotter.theme.cmap = "jet" - self.__background_plotter.background_color = "white" - self.__background_plotter.theme.font.color = "black" + self.__active_plotter.theme.cmap = "jet" + self.__active_plotter.background_color = "white" + self.__active_plotter.theme.font.color = "black" def _display(self): - self.__background_plotter = BackgroundPlotter(title="PyFluent") - self._init_properties() - self._refresh() - self.__background_plotter.add_callback(self._refresh, 100) - self.__background_plotter.app.exec_() + while True: + with self.__condition: + if self.__exit: + break + if ( + not self.__active_plotter or self.__active_plotter._closed + ) and len(self.__graphics) > 0: + plotter_id = next(iter(self.__graphics)) + self.__active_plotter = BackgroundPlotter( + title=f"PyFluent ({plotter_id})" + ) + self._init_properties() + self.__active_plotter.add_callback( + self._get_refresh_for_plotter(plotter_id), + 100, + ) + self.__active_plotter.app.processEvents() + with self.__condition: + for plotter in self.__plotters.values(): + plotter.close() + self.__active_plotter.app.quit() + self.__active_plotter = None + self.__plotters.clear() + self.__condition.notify() def _display_contour(self, obj): if not obj.surfaces_list() or not obj.field(): @@ -114,7 +139,7 @@ def _display_contour(self, obj): boundary_values, ) meta_data = None - plotter = self.__background_plotter + plotter = self.__active_plotter # loop over all meshes for mesh_data in scalar_field_data: @@ -279,36 +304,44 @@ def _display_mesh(self, obj): np.array(mesh_data["vertices"]), faces=np.hstack(mesh_data["faces"]), ) - self.__background_plotter.add_mesh( + self.__active_plotter.add_mesh( mesh, show_edges=obj.show_edges(), color="lightgrey" ) - def _refresh(self): - with self.__condition: - obj = self.__graphics - if not obj: + def _get_refresh_for_plotter(self, plotter_id: str): + def refresh(): + with self.__condition: + obj = self.__graphics.get(plotter_id) + if not obj: + self.__condition.notify() + return + + del self.__graphics[plotter_id] + plotter = self.__active_plotter + plotter.clear() + + camera = plotter.camera.copy() + + if obj.__class__.__name__ == "Mesh": + self._display_mesh(obj) + elif obj.__class__.__name__ == "Surface": + if obj.surface_type.surface_type() == "iso-surface": + self._display_iso_surface(obj) + elif obj.__class__.__name__ == "Contour": + self._display_contour(obj) + + plotter.camera = camera.copy() self.__condition.notify() - return - if self.__exit: - self.__background_plotter.close() - return - - self.__graphics = None - plotter = self.__background_plotter - plotter.clear() - - camera = plotter.camera.copy() - - if obj.__class__.__name__ == "Mesh": - self._display_mesh(obj) - elif obj.__class__.__name__ == "Surface": - if obj.surface_type.surface_type() == "iso-surface": - self._display_iso_surface(obj) - elif obj.__class__.__name__ == "Contour": - self._display_contour(obj) - - plotter.camera = camera.copy() - self.__condition.notify() + + return refresh plotter = _Plotter() + + +def signal_handler(sig, frame): + plotter._exit() + sys.exit(0) + + +signal.signal(signal.SIGINT, signal_handler) diff --git a/ansys/fluent/session.py b/ansys/fluent/session.py index 91f2d280dca..cccfd0f524c 100644 --- a/ansys/fluent/session.py +++ b/ansys/fluent/session.py @@ -1,4 +1,5 @@ import atexit +import itertools from threading import Lock, Thread import grpc @@ -62,12 +63,13 @@ class Session: __all_sessions = [] __on_exit_cbs = [] + __id_iter = itertools.count() def __init__(self, server_info_filepath): address, password = parse_server_info_file(server_info_filepath) self.__channel = grpc.insecure_channel(address) self.__metadata = [("password", password)] - + self.__id = f"session-{next(Session.__id_iter)}" self.__transcript_service: TranscriptService = None self.__transcript_thread: Thread = None self.__lock = Lock() @@ -96,6 +98,10 @@ def __init__(self, server_info_filepath): Session.__all_sessions.append(self) + @property + def id(self): + return self.__id + def setup_settings_objects(self): proxy = SettingsService(self.__channel, self.__metadata) r = flobject.get_root(flproxy=proxy) From 1bd2af4dbc816cff69506b81bd4b3300b2f87ecb Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 15:23:28 +0530 Subject: [PATCH 19/22] Multiple plotters --- .../fluent/postprocessing/pyvista/graphics.py | 12 +++------- .../fluent/postprocessing/pyvista/plotter.py | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/ansys/fluent/postprocessing/pyvista/graphics.py b/ansys/fluent/postprocessing/pyvista/graphics.py index 89820f6c5de..80c63753b61 100644 --- a/ansys/fluent/postprocessing/pyvista/graphics.py +++ b/ansys/fluent/postprocessing/pyvista/graphics.py @@ -35,9 +35,7 @@ def display(self, plotter_id: Optional[str] = None): """ Display mesh graphics. """ - plotter.set_graphics( - self, plotter_id if plotter_id else self.session.id - ) + plotter.plot_graphics(self, plotter_id) class surfaces_list(metaclass=PyLocalPropertyMeta): """ @@ -67,9 +65,7 @@ def display(self, plotter_id: Optional[str] = None): """ Display contour graphics. """ - plotter.set_graphics( - self, plotter_id if plotter_id else self.session.id - ) + plotter.plot_graphics(self, plotter_id) class show_edges(metaclass=PyLocalPropertyMeta): """ @@ -168,9 +164,7 @@ def display(self, plotter_id: Optional[str] = None): """ Display Contour graphics. """ - plotter.set_graphics( - self, plotter_id if plotter_id else self.session.id - ) + plotter.plot_graphics(self, plotter_id) class field(metaclass=PyLocalPropertyMeta): """ diff --git a/ansys/fluent/postprocessing/pyvista/plotter.py b/ansys/fluent/postprocessing/pyvista/plotter.py index 845ca65d2bf..83750e97ec6 100644 --- a/ansys/fluent/postprocessing/pyvista/plotter.py +++ b/ansys/fluent/postprocessing/pyvista/plotter.py @@ -1,6 +1,7 @@ -import threading import sys -import signal +import threading +#import signal +from typing import Optional import numpy as np from pyvistaqt import BackgroundPlotter import pyvista as pv @@ -28,8 +29,8 @@ class _Plotter(metaclass=Singleton): Methods ------- - set_graphics(obj) - Set the graphics object to plot. + plot_graphics(obj, plotter_id: str) + Plot graphics. """ __condition = threading.Condition() @@ -41,10 +42,13 @@ def __init__(self): self.__plotter_thread = None self.__plotters = {} - def set_graphics(self, obj: object, plotter_id: str) -> None: + def plot_graphics( + self, obj: object, plotter_id: Optional[str] = None + ) -> None: if self.__exit: return - + if not plotter_id: + plotter_id = obj.session.id with self.__condition: self.__graphics[plotter_id] = obj self.__active_plotter = self.__plotters.get(plotter_id) @@ -257,9 +261,7 @@ def _display_iso_surface(self, obj): field, dummy_surface_name, (), (), iso_value, () ) - from ansys.fluent.postprocessing.pyvista.graphics import ( - Graphics, - ) + from ansys.fluent.postprocessing.pyvista.graphics import Graphics surfaces_list = list(obj.session.field_data.get_surfaces_info().keys()) if not dummy_surface_name in surfaces_list: @@ -344,4 +346,5 @@ def signal_handler(sig, frame): sys.exit(0) -signal.signal(signal.SIGINT, signal_handler) +# Need to associate ctrl+z signal +# signal.signal(signal.SIGINT, signal_handler) From 200da3e019897fa9651b48eb5d6d333a57c2facd Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 18:02:36 +0530 Subject: [PATCH 20/22] Multiple plotters --- .github/workflows/ci.yml | 40 ++++++++++++++++++++++++++ Makefile | 3 ++ ansys/fluent/services/field_data.py | 4 +-- grpc/ansys-api-fluent-v0-0.0.1.tar.gz | Bin 34493 -> 0 bytes setup.py | 5 +++- 5 files changed, 49 insertions(+), 3 deletions(-) delete mode 100644 grpc/ansys-api-fluent-v0-0.0.1.tar.gz diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b15ef6e9f5f..db686cf303c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,46 @@ jobs: - name: Test import run: make test-import + + test-post-import: + name: Smoke Tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-latest, ubuntu-latest] + python-version: ['3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Linux pip cache + uses: actions/cache@v2 + if: ${{ runner.os == 'Linux' }} + with: + path: ~/.cache/pip + key: Python-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}-${{ hashFiles('requirements_*.txt') }} + restore-keys: | + Python-${{ runner.os }}-${{ matrix.python-version }} + + - name: Window pip cache + uses: actions/cache@v2 + if: ${{ runner.os == 'Windows' }} + with: + path: ~\AppData\Local\pip\Cache + key: Python-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('setup.py') }}-${{ hashFiles('requirements_*.txt') }} + restore-keys: | + Python-${{ runner.os }}-${{ matrix.python-version }} + + - name: Install pyfluent + run: make install + + - name: Test post import + run: make test-post-import build_test: name: Build and Unit Testing diff --git a/Makefile b/Makefile index 0f42b338e51..f0a5b9bf4f0 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,9 @@ install: test-import: @python -c "import ansys.fluent.solver as pyfluent" +test-post-import: + @python -c "import ansys.fluent.postprocessing.pyvista as pv" + unittest: @echo "Running unittest" @pip install -r requirements_test.txt diff --git a/ansys/fluent/services/field_data.py b/ansys/fluent/services/field_data.py index 88762bd92dc..47b1daed815 100644 --- a/ansys/fluent/services/field_data.py +++ b/ansys/fluent/services/field_data.py @@ -41,12 +41,12 @@ class FieldData: get_surfaces_info(self) -> dict Get surfaces information i.e. surface name, id and type. - get_surfaces(surface_ids: List[int], overset_mesh: bool) -> dict + get_surfaces(surface_ids: List[int], overset_mesh: bool) -> List[Dict] Get surfaces data i.e. coordinates and connectivity. def get_scalar_field( surface_ids: List[int], scalar_field: str, node_value: bool, - boundary_value: bool) -> dict + boundary_value: bool) -> List[Dict] Get field data i.e. surface data and associated scalar field values. """ diff --git a/grpc/ansys-api-fluent-v0-0.0.1.tar.gz b/grpc/ansys-api-fluent-v0-0.0.1.tar.gz deleted file mode 100644 index 4fcebd103ddb6bae969ef65a23ea74d99f607923..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34493 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.30.0", + "numpy>=1.21.5", "pyvista>=0.33.2", - "protobuf>=3.12.2" + "protobuf>=3.12.2", + "pyvistaqt>=0.7.0", + "PySide6>=6.2.3", ] is64 = struct.calcsize("P") * 8 == 64 From 9a4cc8ac225a1889de7d0361ad7ff9cd3f878c78 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 19:16:35 +0530 Subject: [PATCH 21/22] Multiple plotters --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db686cf303c..dce30d19c65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: strategy: matrix: os: [windows-latest, ubuntu-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9'] steps: - uses: actions/checkout@v2 From c124109e9389c41f99bda1228076c604f995c066 Mon Sep 17 00:00:00 2001 From: Aseem Jain Date: Fri, 4 Feb 2022 19:42:26 +0530 Subject: [PATCH 22/22] Multiple plotters --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dce30d19c65..53bd39248eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,12 +73,12 @@ jobs: run: make test-import test-post-import: - name: Smoke Tests + name: Post Import runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-latest] - python-version: ['3.7', '3.8', '3.9'] + os: [windows-latest] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2