diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 744346517ede..1fcef4eb4fc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,6 +170,9 @@ jobs: - name: Install pyvistaqt requirements run: make install-pyvistaqt-requirements + - name: Install post requirements + run: make install-post + - name: Unit Testing run: make unittest diff --git a/ansys/fluent/core/meta.py b/ansys/fluent/core/meta.py index 65414b42907a..69cdcead1291 100644 --- a/ansys/fluent/core/meta.py +++ b/ansys/fluent/core/meta.py @@ -8,6 +8,16 @@ from ansys.fluent.core.services.datamodel_tui import PyMenu +class LocalObjectDataExtractor: + def __init__(self, obj): + self.field_info = lambda: obj._get_top_most_parent().session.field_info + self.field_data = lambda: obj._get_top_most_parent().session.field_data + self.surface_api = ( + lambda: obj._get_top_most_parent().session.tui.solver.surface + ) + self.id = lambda: obj._get_top_most_parent().session.id + + class Attribute: VALID_NAMES = ["range", "allowed_values"] @@ -87,7 +97,40 @@ def __new__(cls, name, bases, attrs): return super(PyMenuMeta, cls).__new__(cls, name, bases, attrs) -class PyLocalPropertyMeta(type): +class PyLocalBaseMeta(type): + @classmethod + def __create_get_parent_by_type(cls): + def wrapper(self, obj_type, obj=None): + obj = self if obj is None else obj + parent = None + if getattr(obj, "_parent", None): + if isinstance(obj._parent, obj_type): + return obj._parent + parent = self._get_parent_by_type(obj_type, obj._parent) + return parent + + return wrapper + + @classmethod + def __create_get_top_most_parent(cls): + def wrapper(self, obj=None): + obj = self if obj is None else obj + parent = obj + if getattr(obj, "_parent", None): + parent = self._get_top_most_parent(obj._parent) + return parent + + return wrapper + + def __new__(cls, name, bases, attrs): + attrs[ + "_get_parent_by_type" + ] = cls.__create_get_parent_by_type() + attrs["_get_top_most_parent"] = cls.__create_get_top_most_parent() + return super(PyLocalBaseMeta, cls).__new__(cls, name, bases, attrs) + + +class PyLocalPropertyMeta(PyLocalBaseMeta): """Metaclass for local property classes.""" @classmethod @@ -126,14 +169,8 @@ def wrapper(self, value): @classmethod def __create_init(cls): def wrapper(self, parent): - def get_top_most_parent(obj): - parent = obj - if getattr(obj, "parent", None): - parent = get_top_most_parent(obj.parent) - return parent - - self.get_session = lambda: get_top_most_parent(self).session - self.parent = parent + self._data_extractor = LocalObjectDataExtractor(self) + self._parent = parent self._on_change_cbs = [] annotations = self.__class__.__dict__.get("__annotations__") if isinstance(getattr(self.__class__, "value", None), property): @@ -194,17 +231,17 @@ def __new__(cls, name, bases, attrs): 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(PyLocalPropertyMeta, cls).__new__(cls, name, bases, attrs) -class PyLocalObjectMeta(type): +class PyLocalObjectMeta(PyLocalBaseMeta): """Metaclass for local object classes.""" @classmethod def __create_init(cls): def wrapper(self, parent): - self.parent = parent + self._parent = parent + self._data_extractor = LocalObjectDataExtractor(self) def update(clss): for name, cls in clss.__dict__.items(): @@ -260,7 +297,8 @@ def wrapper(self, value): obj.set_state(val) else: obj.update(val) - wrapper.__doc__ = "Update method." + + wrapper.__doc__ = "Update object." return wrapper # graphics = ansys.fluent.postprocessing.pyvista.Graphics(session1) @@ -330,7 +368,6 @@ def __new__(cls, name, bases, attrs): attrs["__setattr__"] = cls.__create_setattr() attrs["__repr__"] = cls.__create_repr() attrs["update"] = cls.__create_updateitem() - attrs["parent"] = None return super(PyLocalObjectMeta, cls).__new__(cls, name, bases, attrs) @@ -340,8 +377,9 @@ class PyLocalNamedObjectMeta(PyLocalObjectMeta): @classmethod def __create_init(cls): def wrapper(self, name, parent): - self.__name = name - self.parent = parent + self._name = name + self._data_extractor = LocalObjectDataExtractor(self) + self._parent = parent def update(clss): for name, cls in clss.__dict__.items(): @@ -382,7 +420,7 @@ class PyLocalContainer(MutableMapping): """Local container for named objects.""" def __init__(self, parent, object_class): - self.parent = parent + self._parent = parent self.__object_class = object_class self.__collection: dict = {} diff --git a/ansys/fluent/core/services/field_data.py b/ansys/fluent/core/services/field_data.py index cad93b6890f4..9eb4766f7e61 100644 --- a/ansys/fluent/core/services/field_data.py +++ b/ansys/fluent/core/services/field_data.py @@ -43,9 +43,9 @@ def get_fields(self, request): return self.__stub.GetFields(request, metadata=self.__metadata) -class FieldData: +class FieldInfo: """ - Provide the field data. + Provides access to Fluent field info. Methods ------- @@ -62,39 +62,8 @@ 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[int, Dict] - Get surfaces data i.e. coordinates and connectivity. - - get_scalar_field( - surface_ids: List[int], - scalar_field: str, - node_value: Optional[bool] = True, - boundary_value: Optional[bool] = False, - ) -> Dict[int, Dict]: - Get scalar field data i.e. surface data and associated - scalar field values. - - get_vector_field( - surface_ids: List[int], - vector_field: Optional[str] = "velocity", - scalar_field: Optional[str] = "", - node_value: Optional[bool] = False, - ) -> Dict[int, Dict]: - Get vector field data i.e. surface data and associated - scalar and vector field values. - """ - # data mapping - _proto_field_type_to_np_data_type = { - FieldDataProtoModule.FieldType.INT_ARRAY: np.int32, - FieldDataProtoModule.FieldType.LONG_ARRAY: np.int64, - FieldDataProtoModule.FieldType.FLOAT_ARRAY: np.float32, - FieldDataProtoModule.FieldType.DOUBLE_ARRAY: np.float64, - } - _chunk_size = 256 * 1024 - _bytes_stream = True - def __init__(self, service: FieldDataService): self.__service = service @@ -137,7 +106,7 @@ def get_vector_fields_info(self) -> dict: def get_surfaces_info(self) -> dict: request = FieldDataProtoModule.GetSurfacesInfoResponse() response = self.__service.get_surfaces_info(request) - return { + info = { surface_info.surfaceName: { "surface_id": [surf.id for surf in surface_info.surfaceId], "zone_id": surface_info.zoneId.id, @@ -146,6 +115,50 @@ def get_surfaces_info(self) -> dict: } for surface_info in response.surfaceInfo } + return info + + +class FieldData: + """ + Provides access to Fluent field data on surfaces. + + Methods + ------- + get_surfaces(surface_ids: List[int], overset_mesh: bool) -> Dict[int, Dict] + Get surfaces data i.e. coordinates and connectivity. + + get_scalar_field( + surface_ids: List[int], + scalar_field: str, + node_value: Optional[bool] = True, + boundary_value: Optional[bool] = False, + ) -> Dict[int, Dict]: + Get scalar field data i.e. surface data and associated + scalar field values. + + get_vector_field( + surface_ids: List[int], + vector_field: Optional[str] = "velocity", + scalar_field: Optional[str] = "", + node_value: Optional[bool] = False, + ) -> Dict[int, Dict]: + Get vector field data i.e. surface data and associated + scalar and vector field values. + + """ + + # data mapping + _proto_field_type_to_np_data_type = { + FieldDataProtoModule.FieldType.INT_ARRAY: np.int32, + FieldDataProtoModule.FieldType.LONG_ARRAY: np.int64, + FieldDataProtoModule.FieldType.FLOAT_ARRAY: np.float32, + FieldDataProtoModule.FieldType.DOUBLE_ARRAY: np.float64, + } + _chunk_size = 256 * 1024 + _bytes_stream = True + + def __init__(self, service: FieldDataService): + self.__service = service def _extract_fields(self, chunk_iterator): def _extract_field(field_datatype, field_size, chunk_iterator): diff --git a/ansys/fluent/core/session.py b/ansys/fluent/core/session.py index 2131539d75e3..17e3b599a18f 100644 --- a/ansys/fluent/core/session.py +++ b/ansys/fluent/core/session.py @@ -17,10 +17,15 @@ DatamodelService as DatamodelService_TUI, ) from ansys.fluent.core.services.datamodel_tui import PyMenu as PyMenu_TUI -from ansys.fluent.core.services.field_data import FieldData, FieldDataService +from ansys.fluent.core.services.field_data import ( + FieldInfo, + FieldData, + FieldDataService, +) from ansys.fluent.core.services.health_check import HealthCheckService from ansys.fluent.core.services.scheme_eval import ( - SchemeEval, SchemeEvalService + SchemeEval, + SchemeEvalService, ) from ansys.fluent.core.services.settings import SettingsService from ansys.fluent.core.services.transcript import TranscriptService @@ -28,6 +33,7 @@ from ansys.fluent.core.services.events import EventsService from ansys.fluent.core.solver.events_manager import EventsManager + def _parse_server_info_file(filename: str): with open(filename, encoding="utf-8") as f: lines = f.readlines() @@ -144,8 +150,9 @@ def __init__( if not port: port = os.getenv("PYFLUENT_FLUENT_PORT") if not port: - raise RuntimeError("The port to connect to Fluent " - "session is not provided.") + raise ValueError( + "The port to connect to Fluent session is not provided." + ) self._channel = grpc.insecure_channel(f"{ip}:{port}") self._metadata: List[Tuple[str, str]] = [] self._id = f"session-{next(Session._id_iter)}" @@ -170,6 +177,7 @@ def __init__( self._field_data_service = FieldDataService( self._channel, self._metadata ) + self.field_info = FieldInfo(self._field_data_service) self.field_data = FieldData(self._field_data_service) self.tui = Session.Tui(self._datamodel_service_tui) diff --git a/ansys/fluent/core/utils/dump_session_data.py b/ansys/fluent/core/utils/dump_session_data.py new file mode 100644 index 000000000000..b23b4d31a9ea --- /dev/null +++ b/ansys/fluent/core/utils/dump_session_data.py @@ -0,0 +1,86 @@ +"""Module providing dump session data functionality.""" +import pickle + + +def dump_session_data( + session, file_path: str, fields: list = [], surfaces: list = [] +): + """ + Dump session data. + + Parameters + ---------- + session : + Session object. + file_path: str + Session dump file path. + fields: list, optional + List of fields to write. If empty then all fields will be written. + surfaces: list, optional + List of surfaces to write. If empty then all surfaces will be written. + """ + session_data = {} + session_data["scalar_fields_info"] = { + k: v + for k, v in session.field_info.get_fields_info().items() + if (not fields or v["solver_name"] in fields) + } + session_data["surfaces_info"] = { + k: v + for k, v in session.field_info.get_surfaces_info().items() + if (not surfaces or k in surfaces) + } + session_data[ + "vector_fields_info" + ] = session.field_info.get_vector_fields_info() + if not fields: + fields = [ + v["solver_name"] + for k, v in session_data["scalar_fields_info"].items() + ] + surfaces_id = [ + v["surface_id"][0] for k, v in session_data["surfaces_info"].items() + ] + session_data["range"] = {} + for field in fields: + session_data["range"][field] = {} + for surface in surfaces_id: + session_data["range"][field][surface] = {} + session_data["range"][field][surface][ + "node_value" + ] = session.field_info.get_range(field, True, [surface]) + session_data["range"][field][surface][ + "cell_value" + ] = session.field_info.get_range(field, False, [surface]) + + session_data["scalar-field"] = {} + for field in fields: + session_data["scalar-field"][field] = {} + for surface in surfaces_id: + session_data["scalar-field"][field][surface] = {} + session_data["scalar-field"][field][surface][ + "node_value" + ] = session.field_data.get_scalar_field([surface], field, True)[ + surface + ] + session_data["scalar-field"][field][surface][ + "cell_value" + ] = session.field_data.get_scalar_field([surface], field, False)[ + surface + ] + + session_data["surfaces"] = {} + for surface in surfaces_id: + session_data["surfaces"][surface] = session.field_data.get_surfaces( + [surface] + )[surface] + + session_data["vector-field"] = {} + for surface in surfaces_id: + session_data["vector-field"][ + surface + ] = session.field_data.get_vector_field([surface])[surface] + + pickle_obj = open(file_path, "wb") + pickle.dump(session_data, pickle_obj) + pickle_obj.close() diff --git a/ansys/fluent/core/utils/generic.py b/ansys/fluent/core/utils/generic.py index 4e859302a21d..b7cb1c06613d 100644 --- a/ansys/fluent/core/utils/generic.py +++ b/ansys/fluent/core/utils/generic.py @@ -49,7 +49,6 @@ def cb(*args, **kwargs): def in_notebook(): """Function to check if application is running in notebook.""" - try: from IPython import get_ipython diff --git a/ansys/fluent/post/matplotlib/__init__.py b/ansys/fluent/post/matplotlib/__init__.py index a025cb1da0f8..3a7876666924 100644 --- a/ansys/fluent/post/matplotlib/__init__.py +++ b/ansys/fluent/post/matplotlib/__init__.py @@ -2,4 +2,4 @@ from ansys.fluent.post.matplotlib.\ matplot_windows_manager import matplot_windows_manager # noqa: F401 -from ansys.fluent.post.matplotlib.matplot_objects import XYPlots # noqa: F401 +from ansys.fluent.post.matplotlib.matplot_objects import Plots # noqa: F401 diff --git a/ansys/fluent/post/matplotlib/matplot_objects.py b/ansys/fluent/post/matplotlib/matplot_objects.py index 82efc619e018..13ded664fc45 100644 --- a/ansys/fluent/post/matplotlib/matplot_objects.py +++ b/ansys/fluent/post/matplotlib/matplot_objects.py @@ -1,5 +1,6 @@ """Module providing post objects for Matplotlib.""" - +import inspect +import sys from typing import Optional from ansys.fluent.core.meta import PyLocalContainer @@ -7,22 +8,34 @@ from ansys.fluent.post.post_object_defns import XYPlotDefn -class XYPlots(PyLocalContainer): - """XYPlot objects provider.""" +class Plots: + """Plot objects provider.""" _sessions_state = {} def __init__(self, session): - """Instantiate XYPlots, containter of XYPlot.""" - session_state = XYPlots._sessions_state.get(session.id) + """Instantiate Plots, container of plot objects.""" + session_state = Plots._sessions_state.get(session.id if session else 1) if not session_state: session_state = self.__dict__ - XYPlots._sessions_state[session.id] = session_state + Plots._sessions_state[session.id if session else 1] = session_state self.session = session - super().__init__(None, XYPlot) + self._init_module(self, sys.modules[__name__]) else: self.__dict__ = session_state + def _init_module(self, obj, mod): + for name, cls in mod.__dict__.items(): + + if cls.__class__.__name__ in ( + "PyLocalNamedObjectMetaAbstract", + ) and not inspect.isabstract(cls): + setattr( + obj, + cls.PLURAL, + PyLocalContainer(self, cls), + ) + class XYPlot(XYPlotDefn): """XY Plot.""" diff --git a/ansys/fluent/post/matplotlib/matplot_windows_manager.py b/ansys/fluent/post/matplotlib/matplot_windows_manager.py index 317105a0d7c9..5b3b36405931 100644 --- a/ansys/fluent/post/matplotlib/matplot_windows_manager.py +++ b/ansys/fluent/post/matplotlib/matplot_windows_manager.py @@ -125,8 +125,9 @@ def _get_xy_plot_data(self): "xlabel": "position", "ylabel": field, } - field_data = obj.parent.session.field_data - surfaces_info = field_data.get_surfaces_info() + field_info = obj._data_extractor.field_info() + field_data = obj._data_extractor.field_data() + surfaces_info = field_info.get_surfaces_info() surface_ids = [ id for surf in obj.surfaces_list() @@ -357,8 +358,7 @@ def _get_windows_id( if not window.plotter.is_closed() and ( not session_id - or session_id - == window.graphics_object.parent.parent.session.id + or session_id == window.post_object._data_extractor.id() ) ] if not windows_id or window_id in windows_id diff --git a/ansys/fluent/post/post_object_defns.py b/ansys/fluent/post/post_object_defns.py index caa70275fe61..4e925250a175 100644 --- a/ansys/fluent/post/post_object_defns.py +++ b/ansys/fluent/post/post_object_defns.py @@ -54,6 +54,8 @@ class Vector(NamedTuple): class XYPlotDefn(PlotDefn): """XYPlot Definition.""" + PLURAL = "XYPlots" + class node_values(metaclass=PyLocalPropertyMeta): """Show nodal data.""" @@ -79,8 +81,8 @@ def allowed_values(self): """Y axis function allowed values.""" return [ v["solver_name"] - for k, v in self.get_session() - .field_data.get_fields_info() + for k, v in self._data_extractor.field_info() + .get_fields_info() .items() ] @@ -103,7 +105,7 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): def allowed_values(self): """Surface list allowed values.""" return list( - self.get_session().field_data.get_surfaces_info().keys() + self._data_extractor.field_info().get_surfaces_info().keys() ) @@ -121,7 +123,7 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): def allowed_values(self): """Surface list allowed values.""" return list( - (self.get_session().field_data.get_surfaces_info().keys()) + (self._data_extractor.field_info().get_surfaces_info().keys()) ) class show_edges(metaclass=PyLocalPropertyMeta): @@ -174,10 +176,10 @@ class field(metaclass=PyLocalPropertyMeta): @Attribute def allowed_values(self): """Field allowed values.""" - field_data = self.get_session().field_data + field_info = self._data_extractor.field_info() return [ v["solver_name"] - for k, v in field_data.get_fields_info().items() + for k, v in field_info.get_fields_info().items() ] class rendering(metaclass=PyLocalPropertyMeta): @@ -196,7 +198,7 @@ class iso_value(metaclass=PyLocalPropertyMeta): _value: float def _reset_on_change(self): - return [self.parent.field] + return [self._parent.field] @property def value(self): @@ -213,9 +215,9 @@ def value(self, value): @Attribute def range(self): """Iso value range.""" - field = self.parent.field() + field = self._parent.field() if field: - return self.get_session().field_data.get_range( + return self._data_extractor.field_info().get_range( field, True ) @@ -233,10 +235,10 @@ class field(metaclass=PyLocalPropertyMeta): @Attribute def allowed_values(self): """Field allowed values.""" - field_data = self.get_session().field_data + field_info = self._data_extractor.field_info() return [ v["solver_name"] - for k, v in field_data.get_fields_info().items() + for k, v in field_info.get_fields_info().items() ] class surfaces_list(metaclass=PyLocalPropertyMeta): @@ -248,7 +250,7 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): def allowed_values(self): """Surfaces list allowed values.""" return list( - self.get_session().field_data.get_surfaces_info().keys() + self._data_extractor.field_info().get_surfaces_info().keys() ) class filled(metaclass=PyLocalPropertyMeta): @@ -276,17 +278,17 @@ class show_edges(metaclass=PyLocalPropertyMeta): value: bool = False - class range_option(metaclass=PyLocalObjectMeta): + class range (metaclass=PyLocalObjectMeta): """Specify range options.""" def _availability(self, name): if name == "auto_range_on": - return self.range_option() == "auto-range-on" + return self.option() == "auto-range-on" if name == "auto_range_off": - return self.range_option() == "auto-range-off" + return self.option() == "auto-range-off" return True - class range_option(metaclass=PyLocalPropertyMeta): + class option(metaclass=PyLocalPropertyMeta): """Range option.""" value: str = "auto-range-on" @@ -319,19 +321,22 @@ class minimum(metaclass=PyLocalPropertyMeta): def _reset_on_change(self): return [ - self.parent.parent.parent.field, - self.parent.parent.parent.node_values, + self._get_parent_by_type(ContourDefn).field, + self._get_parent_by_type(ContourDefn).node_values, ] @property def value(self): """Range minimum property setter.""" if getattr(self, "_value", None) is None: - field = self.parent.parent.parent.field() + field = self._get_parent_by_type(ContourDefn).field() if field: - field_data = self.get_session().field_data - field_range = field_data.get_range( - field, self.parent.parent.parent.node_values() + field_info = self._data_extractor.field_info() + field_range = field_info.get_range( + field, + self._get_parent_by_type( + ContourDefn + ).node_values(), ) self._value = field_range[0] return self._value @@ -347,20 +352,22 @@ class maximum(metaclass=PyLocalPropertyMeta): def _reset_on_change(self): return [ - self.parent.parent.parent.field, - self.parent.parent.parent.node_values, + self._get_parent_by_type(ContourDefn).field, + self._get_parent_by_type(ContourDefn).node_values, ] @property def value(self): """Range maximum property setter.""" if getattr(self, "_value", None) is None: - field = self.parent.parent.parent.field() + field = self._get_parent_by_type(ContourDefn).field() if field: - field_data = self.get_session().field_data - field_range = field_data.get_range( + field_info = self._data_extractor.field_info() + field_range = field_info.get_range( field, - self.parent.parent.parent.node_values(), + self._get_parent_by_type( + ContourDefn + ).node_values(), ) self._value = field_range[1] @@ -385,7 +392,9 @@ class vectors_of(metaclass=PyLocalPropertyMeta): def allowed_values(self): """Vectors of allowed values.""" return list( - self.get_session().field_data.get_vector_fields_info().keys() + self._data_extractor.field_info() + .get_vector_fields_info() + .keys() ) class surfaces_list(metaclass=PyLocalPropertyMeta): @@ -397,7 +406,7 @@ class surfaces_list(metaclass=PyLocalPropertyMeta): def allowed_values(self): """Surface list allowed values.""" return list( - self.get_session().field_data.get_surfaces_info().keys() + self._data_extractor.field_info().get_surfaces_info().keys() ) class scale(metaclass=PyLocalPropertyMeta): @@ -415,17 +424,17 @@ class show_edges(metaclass=PyLocalPropertyMeta): value: bool = False - class range_option(metaclass=PyLocalObjectMeta): + class range(metaclass=PyLocalObjectMeta): """Specify range options.""" def _availability(self, name): if name == "auto_range_on": - return self.range_option() == "auto-range-on" + return self.option() == "auto-range-on" if name == "auto_range_off": - return self.range_option() == "auto-range-off" + return self.option() == "auto-range-off" return True - class range_option(metaclass=PyLocalPropertyMeta): + class option(metaclass=PyLocalPropertyMeta): """Range option.""" value: str = "auto-range-on" @@ -460,8 +469,8 @@ class minimum(metaclass=PyLocalPropertyMeta): def value(self): """Range minimum property setter.""" if getattr(self, "_value", None) is None: - field_data = self.self.get_session().field_data - field_range = field_data.get_range( + field_info = self._data_extractor.field_info() + field_range = field_info.get_range( "velocity-magnitude", False, ) @@ -481,8 +490,8 @@ class maximum(metaclass=PyLocalPropertyMeta): def value(self): """Range maximum property setter.""" if getattr(self, "_value", None) is None: - field_data = self.self.get_session().field_data - field_range = field_data.get_range( + field_info = self._data_extractor.field_info() + field_range = field_info.get_range( "velocity-magnitude", False, ) diff --git a/ansys/fluent/post/pyvista/pyvista_objects.py b/ansys/fluent/post/pyvista/pyvista_objects.py index 2f48581b1d10..dcc0025897d3 100644 --- a/ansys/fluent/post/pyvista/pyvista_objects.py +++ b/ansys/fluent/post/pyvista/pyvista_objects.py @@ -23,10 +23,14 @@ class Graphics: def __init__(self, session): """Instantiate Graphics, containter of graphics objects.""" - session_state = Graphics._sessions_state.get(session.id) + session_state = Graphics._sessions_state.get( + session.id if session else 1 + ) if not session_state: session_state = self.__dict__ - Graphics._sessions_state[session.id] = session_state + Graphics._sessions_state[ + session.id if session else 1 + ] = session_state self.session = session self._init_module(self, sys.modules[__name__]) else: diff --git a/ansys/fluent/post/pyvista/pyvista_windows_manager.py b/ansys/fluent/post/pyvista/pyvista_windows_manager.py index 146dfbcbf8b7..c0951cb8de6a 100644 --- a/ansys/fluent/post/pyvista/pyvista_windows_manager.py +++ b/ansys/fluent/post/pyvista/pyvista_windows_manager.py @@ -91,10 +91,11 @@ def _display_vector( if not obj.surfaces_list(): raise RuntimeError("Vector definition is incomplete.") - field_data = obj.parent.parent.session.field_data + field_info = obj._data_extractor.field_info() + field_data = obj._data_extractor.field_data() # surface ids - surfaces_info = field_data.get_surfaces_info() + surfaces_info = field_info.get_surfaces_info() surface_ids = [ id for surf in obj.surfaces_list() @@ -128,8 +129,8 @@ def _display_vector( ) mesh.cell_data["vectors"] = mesh_data["vector"] velocity_magnitude = np.linalg.norm(mesh_data["vector"], axis=1) - if obj.range_option.range_option() == "auto-range-off": - auto_range_off = obj.range_option.auto_range_off + if obj.range.option() == "auto-range-off": + auto_range_off = obj.range.auto_range_off range = [auto_range_off.minimum(), auto_range_off.maximum()] if auto_range_off.clip_to_range(): velocity_magnitude = np.ma.masked_outside( @@ -138,11 +139,11 @@ def _display_vector( auto_range_off.maximum(), ).filled(fill_value=0) else: - auto_range_on = obj.range_option.auto_range_on + auto_range_on = obj.range.auto_range_on if auto_range_on.global_range(): - range = field_data.get_range(field, False) + range = field_info.get_range(field, False) else: - range = field_data.get_range(field, False, surface_ids) + range = field_info.get_range(field, False, surface_ids) if obj.skip(): vmag = np.zeros(velocity_magnitude.size) @@ -171,7 +172,7 @@ def _display_contour( # contour properties field = obj.field() - range_option = obj.range_option.range_option() + range_option = obj.range.option() filled = obj.filled() contour_lines = obj.contour_lines() node_values = obj.node_values() @@ -180,8 +181,9 @@ def _display_contour( # scalar bar properties scalar_bar_args = self._scalar_bar_default_properties() - field_data = obj.parent.parent.session.field_data - surfaces_info = field_data.get_surfaces_info() + field_info = obj._data_extractor.field_info() + field_data = obj._data_extractor.field_data() + surfaces_info = field_info.get_surfaces_info() surface_ids = [ id for surf in obj.surfaces_list() @@ -213,7 +215,7 @@ def _display_contour( else: mesh.cell_data[field] = mesh_data[field] if range_option == "auto-range-off": - auto_range_off = obj.range_option.auto_range_off + auto_range_off = obj.range.auto_range_off if auto_range_off.clip_to_range(): if np.min(mesh[field]) < auto_range_off.maximum(): maximum_below = mesh.clip_scalar( @@ -261,12 +263,12 @@ def _display_contour( ): plotter.add_mesh(mesh.contour(isosurfaces=20)) else: - auto_range_on = obj.range_option.auto_range_on + auto_range_on = obj.range.auto_range_on if auto_range_on.global_range(): if filled: plotter.add_mesh( mesh, - clim=field_data.get_range(field, False), + clim=field_info.get_range(field, False), scalars=field, show_edges=obj.show_edges(), scalar_bar_args=scalar_bar_args, @@ -297,22 +299,22 @@ def _display_iso_surface( raise RuntimeError("Iso surface definition is incomplete.") dummy_surface_name = "_dummy_iso_surface_for_pyfluent" - field_data = obj.parent.parent.session.field_data - surfaces_list = list(field_data.get_surfaces_info().keys()) + field_info = obj._data_extractor.field_info() + surfaces_list = list(field_info.get_surfaces_info().keys()) iso_value = obj.surface_type.iso_surface.iso_value() if dummy_surface_name in surfaces_list: - obj.parent.parent.session.tui.solver.surface.delete_surface( + obj._data_extractor.surface_api().delete_surface( dummy_surface_name ) - obj.parent.parent.session.tui.solver.surface.iso_surface( + obj._data_extractor.surface_api().iso_surface( field, dummy_surface_name, (), (), iso_value, () ) - surfaces_list = list(field_data.get_surfaces_info().keys()) + surfaces_list = list(field_info.get_surfaces_info().keys()) if dummy_surface_name not in surfaces_list: raise RuntimeError("Iso surface creation failed.") - post_session = obj.parent.parent + post_session = obj._get_top_most_parent() if obj.surface_type.iso_surface.rendering() == "mesh": mesh = post_session.Meshes[dummy_surface_name] mesh.surfaces_list = [dummy_surface_name] @@ -324,20 +326,19 @@ def _display_iso_surface( 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 + contour.range.auto_range_on.global_range = True self._display_contour(contour, plotter) del post_session.Contours[dummy_surface_name] - obj.parent.parent.session.tui.solver.surface.delete_surface( - dummy_surface_name - ) + obj._data_extractor.surface_api().delete_surface(dummy_surface_name) def _display_mesh( self, obj, plotter: Union[BackgroundPlotter, pv.Plotter] ): if not obj.surfaces_list(): raise RuntimeError("Mesh definition is incomplete.") - field_data = obj.parent.parent.session.field_data - surfaces_info = field_data.get_surfaces_info() + field_info = obj._data_extractor.field_info() + field_data = obj._data_extractor.field_data() + surfaces_info = field_info.get_surfaces_info() surface_ids = [ id for surf in obj.surfaces_list() @@ -608,7 +609,8 @@ def _open_and_plot_console(self, obj: object, window_id: str) -> None: self._post_object = obj if not self._plotter_thread: - Session._monitor_thread.cbs.append(self._exit) + if Session._monitor_thread: + Session._monitor_thread.cbs.append(self._exit) self._plotter_thread = threading.Thread( target=self._display, args=() ) @@ -648,7 +650,7 @@ def _get_windows_id( and ( not session_id or session_id - == window.post_object.parent.parent.session.id + == window.post_object._data_extractor.id() ) ] if not windows_id or window_id in windows_id diff --git a/tests/session.dump b/tests/session.dump new file mode 100644 index 000000000000..daccfbfc3f18 Binary files /dev/null and b/tests/session.dump differ diff --git a/tests/test_post.py b/tests/test_post.py new file mode 100644 index 000000000000..05b479197e6e --- /dev/null +++ b/tests/test_post.py @@ -0,0 +1,301 @@ +from typing import Dict, List, Optional +import pickle +import pytest +from pathlib import Path +from ansys.fluent.post.pyvista import Graphics +from ansys.fluent.post.matplotlib import Plots + + +@pytest.fixture(autouse=True) +def patch_mock_data_extractor(mocker) -> None: + mocker.patch( + "ansys.fluent.core.meta.LocalObjectDataExtractor", + MockLocalObjectDataExtractor, + ) + + +class MockFieldData: + def __init__(self, solver_data): + self._session_data = solver_data + + def get_scalar_field( + self, + surface_ids: List[int], + scalar_field: str, + node_value: Optional[bool] = True, + boundary_value: Optional[bool] = False, + ) -> Dict[int, Dict]: + return { + surface_id: self._session_data["scalar-field"][scalar_field][ + surface_id + ]["node_value" if node_value else "cell_value"] + for surface_id in surface_ids + } + + def get_vector_field( + self, + surface_ids: List[int], + vector_field: Optional[str] = "velocity", + scalar_field: Optional[str] = "", + node_value: Optional[bool] = False, + ) -> Dict[int, Dict]: + return { + surface_id: self._session_data["vector-field"][surface_id] + for surface_id in surface_ids + } + + def get_surfaces( + self, surface_ids: List[int], overset_mesh: bool = False + ) -> Dict[int, Dict]: + return { + surface_id: self._session_data["surfaces"][surface_id] + for surface_id in surface_ids + } + + +class MockFieldInfo: + def __init__(self, solver_data): + self._session_data = solver_data + + def get_range( + self, field: str, node_value: bool = False, surface_ids: List[int] = [] + ) -> List[float]: + if not surface_ids: + surface_ids = [ + v["surface_id"][0] + for k, v in self._session_data["surfaces_info"].items() + ] + minimum, maximum = None, None + for surface_id in surface_ids: + range = self._session_data["range"][field][surface_id][ + "node_value" if node_value else "cell_value" + ] + minimum = min(range[0], minimum) if minimum else range[0] + maximum = max(range[1], maximum) if maximum else range[1] + return [minimum, maximum] + + def get_fields_info(self) -> dict: + return self._session_data["scalar_fields_info"] + + def get_vector_fields_info(self) -> dict: + return self._session_data["vector_fields_info"] + + def get_surfaces_info(self) -> dict: + return self._session_data["surfaces_info"] + + +class MockLocalObjectDataExtractor: + _session_data = None + _session_dump = "tests//session.dump" + + def __init__(self, obj=None): + if not MockLocalObjectDataExtractor._session_data: + pickle_obj = open( + str( + Path(MockLocalObjectDataExtractor._session_dump).resolve() + ), + "rb", + ) + MockLocalObjectDataExtractor._session_data = pickle.load( + pickle_obj + ) + pickle_obj.close() + self.field_info = lambda: MockFieldInfo( + MockLocalObjectDataExtractor._session_data + ) + self.field_data = lambda: MockFieldData( + MockLocalObjectDataExtractor._session_data + ) + self.id = lambda: 1 + + +def test_create_graphics_objects(): + pyvista_graphics1 = Graphics(session=None) + pyvista_graphics2 = Graphics(session=None) + pyvista_graphics1.Contours["contour-1"] + pyvista_graphics2.Contours["contour-2"] + + assert pyvista_graphics1 is not pyvista_graphics2 + assert pyvista_graphics1.Contours is pyvista_graphics2.Contours + assert list(pyvista_graphics1.Contours) == ["contour-1", "contour-2"] + + +def test_contour_object(): + + pyvista_graphics = Graphics(session=None) + contour1 = pyvista_graphics.Contours["contour-1"] + field_info = contour1._data_extractor.field_info() + + assert contour1.surfaces_list.allowed_values == list( + field_info.get_surfaces_info().keys() + ) + + with pytest.raises(ValueError) as value_error: + contour1.surfaces_list = "surface_does_not_exist" + + with pytest.raises(ValueError) as value_error: + contour1.surfaces_list = ["surface_does_not_exist"] + contour1.surfaces_list = contour1.surfaces_list.allowed_values + + assert contour1.field.allowed_values == [ + v["solver_name"] for k, v in field_info.get_fields_info().items() + ] + + # Important. Because there is no type checking so following passes. + contour1.field = [contour1.field.allowed_values[0]] + + contour1.field = contour1.field.allowed_values[0] + with pytest.raises(ValueError) as value_error: + contour1.field = "field_does_not_exist" + + # Important. Because there is no type checking so following passes. + contour1.node_values = "value should be boolean" + + contour1.range.option = "auto-range-on" + assert contour1.range.auto_range_off is None + + contour1.range.option = "auto-range-off" + assert contour1.range.auto_range_on is None + + contour1.node_values = True + contour1.field = "temperature" + surfaces_id = [ + v["surface_id"][0] + for k, v in field_info.get_surfaces_info().items() + if k in contour1.surfaces_list() + ] + + range = field_info.get_range( + contour1.field(), contour1.node_values(), surfaces_id + ) + assert range[0] == pytest.approx(contour1.range.auto_range_off.minimum()) + assert range[1] == pytest.approx(contour1.range.auto_range_off.maximum()) + + contour1.node_values = False + range = field_info.get_range( + contour1.field(), contour1.node_values(), surfaces_id + ) + assert range[0] == pytest.approx(contour1.range.auto_range_off.minimum()) + assert range[1] == pytest.approx(contour1.range.auto_range_off.maximum()) + + contour1.field = "pressure" + range = field_info.get_range( + contour1.field(), contour1.node_values(), surfaces_id + ) + assert range[0] == pytest.approx(contour1.range.auto_range_off.minimum()) + assert range[1] == pytest.approx(contour1.range.auto_range_off.maximum()) + + +def test_vector_object(): + + pyvista_graphics = Graphics(session=None) + vector1 = pyvista_graphics.Vectors["contour-1"] + field_info = vector1._data_extractor.field_info() + + assert vector1.surfaces_list.allowed_values == list( + field_info.get_surfaces_info().keys() + ) + + with pytest.raises(ValueError) as value_error: + vector1.surfaces_list = "surface_does_not_exist" + + with pytest.raises(ValueError) as value_error: + vector1.surfaces_list = ["surface_does_not_exist"] + + vector1.surfaces_list = vector1.surfaces_list.allowed_values + + vector1.range.option = "auto-range-on" + assert vector1.range.auto_range_off is None + + vector1.range.option = "auto-range-off" + assert vector1.range.auto_range_on is None + + surfaces_id = [ + v["surface_id"][0] + for k, v in field_info.get_surfaces_info().items() + if k in vector1.surfaces_list() + ] + + range = field_info.get_range("velocity-magnitude", False) + assert range == pytest.approx( + [ + vector1.range.auto_range_off.minimum(), + vector1.range.auto_range_off.maximum(), + ] + ) + + +def test_surface_object(): + + pyvista_graphics = Graphics(session=None) + surf1 = pyvista_graphics.Surfaces["surf-1"] + field_info = surf1._data_extractor.field_info() + + surf1.surface_type.surface_type = "iso-surface" + iso_surf = surf1.surface_type.iso_surface + + assert iso_surf.field.allowed_values == [ + v["solver_name"] for k, v in field_info.get_fields_info().items() + ] + + # Important. Because there is no type checking so following passes. + iso_surf.field = [iso_surf.field.allowed_values[0]] + + with pytest.raises(ValueError) as value_error: + iso_surf.field = "field_does_not_exist" + + iso_surf.field = "temperature" + range = field_info.get_range(iso_surf.field(), True) + assert range[0] == pytest.approx(iso_surf.iso_value()) + + with pytest.raises(ValueError) as value_error: + iso_surf.iso_value = range[1] + 0.001 + + with pytest.raises(ValueError) as value_error: + iso_surf.iso_value = range[0] - 0.001 + + iso_surf.field = "pressure" + range = field_info.get_range(iso_surf.field(), True) + assert range[0] == pytest.approx(iso_surf.iso_value()) + + +def test_create_plot_objects(): + matplotlib_plots1 = Plots(session=None) + matplotlib_plots2 = Plots(session=None) + matplotlib_plots1.XYPlots["p-1"] + matplotlib_plots2.XYPlots["p-2"] + + assert matplotlib_plots1 is not matplotlib_plots2 + assert matplotlib_plots1.XYPlots is matplotlib_plots2.XYPlots + assert list(matplotlib_plots1.XYPlots) == ["p-1", "p-2"] + + +def test_xyplot_object(): + + matplotlib_plots = Plots(session=None) + p1 = matplotlib_plots.XYPlots["p-1"] + field_info = p1._data_extractor.field_info() + + assert p1.surfaces_list.allowed_values == list( + field_info.get_surfaces_info().keys() + ) + + with pytest.raises(ValueError) as value_error: + p1.surfaces_list = "surface_does_not_exist" + + with pytest.raises(ValueError) as value_error: + p1.surfaces_list = ["surface_does_not_exist"] + + p1.surfaces_list = p1.surfaces_list.allowed_values + + assert p1.y_axis_function.allowed_values == [ + v["solver_name"] for k, v in field_info.get_fields_info().items() + ] + + # Important. Because there is no type checking so following passes. + p1.y_axis_function = [p1.y_axis_function.allowed_values[0]] + + p1.y_axis_function = p1.y_axis_function.allowed_values[0] + + with pytest.raises(ValueError) as value_error: + p1.y_axis_function = "field_does_not_exist"