diff --git a/doc/source/api/mapdl.rst b/doc/source/api/mapdl.rst index 37e11757316..2be81050b13 100644 --- a/doc/source/api/mapdl.rst +++ b/doc/source/api/mapdl.rst @@ -51,3 +51,37 @@ Latest 2021R1 and newer features mapdl_grpc.MapdlGrpc.math mapdl_grpc.MapdlGrpc.mute mapdl_grpc.MapdlGrpc.upload + + +Mapdl Information Class +~~~~~~~~~~~~~~~~~~~~~~~ + +.. currentmodule:: ansys.mapdl.core.misc + +.. autoclass:: ansys.mapdl.core.misc.Information + +.. autosummary:: + :toctree: _autosummary + + Information.product + Information.mapdl_version + Information.pymapdl_version + Information.products + Information.preprocessing_capabilities + Information.aux_capabilities + Information.solution_options + Information.post_capabilities + Information.title + Information.titles + Information.stitles + Information.units + Information.scratch_memory_status + Information.database_status + Information.config_values + Information.global_status + Information.job_information + Information.model_information + Information.boundary_condition_information + Information.routine_information + Information.solution_options_configuration + Information.load_step_options diff --git a/src/ansys/mapdl/core/mapdl.py b/src/ansys/mapdl/core/mapdl.py index 55ecaa21c8a..a4c82327244 100644 --- a/src/ansys/mapdl/core/mapdl.py +++ b/src/ansys/mapdl/core/mapdl.py @@ -31,6 +31,7 @@ from ansys.mapdl.core.errors import MapdlInvalidRoutineError, MapdlRuntimeError from ansys.mapdl.core.inline_functions import Query from ansys.mapdl.core.misc import ( + Information, last_created, load_file, random_string, @@ -201,6 +202,8 @@ def __init__( self._wrap_listing_functions() + self._info = Information(self) + @property def print_com(self): return self._print_com @@ -570,27 +573,12 @@ def clear(self, *args, **kwargs): @supress_logging def __str__(self): - try: - if self._exited: - return "MAPDL exited" - stats = self.slashstatus("PROD") - except Exception: # pragma: no cover - return "MAPDL exited" - - st = stats.find("*** Products ***") - en = stats.find("*** PrePro") - product = "\n".join(stats[st:en].splitlines()[1:]).strip() - - # get product version - stats = self.slashstatus("TITLE") - st = stats.find("RELEASE") - en = stats.find("INITIAL", st) - mapdl_version = stats[st:en].split("CUSTOMER")[0].strip() - - info = [f"Product: {product}"] - info.append(f"MAPDL Version: {mapdl_version}") - info.append(f"PyMAPDL Version: {pymapdl.__version__}\n") - return "\n".join(info) + return self.info.__str__() + + @property + def info(self): + """General information""" + return self._info @property def geometry(self): @@ -2420,11 +2408,10 @@ def run(self, command, write_to_log=True, mute=None, **kwargs) -> str: msg = f"{cmd_} is ignored: {INVAL_COMMANDS_SILENT[cmd_]}." self._log.info(msg) - # This very likely won't be recorded anywhere. - + # This, very likely, won't be recorded anywhere. # But just in case, I'm adding info as /com command = ( - f"/com, PyAnsys: {msg}" # Using '!' makes the output of '_run' empty + f"/com, PyMAPDL: {msg}" # Using '!' makes the output of '_run' empty ) if command[:3].upper() in INVAL_COMMANDS: diff --git a/src/ansys/mapdl/core/misc.py b/src/ansys/mapdl/core/misc.py index edcdd7fe05f..222ee0721f4 100644 --- a/src/ansys/mapdl/core/misc.py +++ b/src/ansys/mapdl/core/misc.py @@ -4,15 +4,19 @@ import os import platform import random +import re import socket import string import sys import tempfile from threading import Thread +import weakref import numpy as np import scooby +from ansys.mapdl import core as pymapdl + # path of this module MODULE_PATH = os.path.dirname(inspect.getfile(inspect.currentframe())) @@ -425,3 +429,425 @@ def check_valid_start_instance(start_instance): ) return start_instance.lower() == "true" + + +def update_information_first(update=False): + """ + Decorator to wrap :class:`Information ` + methods to force update the fields when accessed. + + Parameters + ---------- + update : bool, optional + If ``True``, the class information is updated by calling ``/STATUS`` + before accessing the methods. By default ``False`` + """ + + def decorator(function): + @wraps(function) + def wrapper(self, *args, **kwargs): + if update or not self._stats: + self._update() + return function(self, *args, **kwargs) + + return wrapper + + return decorator + + +class Information: + """ + This class provide some MAPDL information from ``/STATUS`` MAPDL command. + + It is also the object that is called when you issue ``print(mapdl)``, + which means ``print`` calls ``mapdl.info.__str__()``. + + Notes + ----- + You cannot directly modify the values of this class. + + Some of the results are cached for later calls. + + Examples + -------- + >>> mapdl.info + Product: Ansys Mechanical Enterprise + MAPDL Version: 21.2 + ansys.mapdl Version: 0.62.dev0 + + >>> print(mapdl) + Product: Ansys Mechanical Enterprise + MAPDL Version: 21.2 + ansys.mapdl Version: 0.62.dev0 + + >>> mapdl.info.product + 'Ansys Mechanical Enterprise' + + >>> info = mapdl.info + >>> info.mapdl_version + 'RELEASE 2021 R2 BUILD 21.2 UPDATE 20210601' + + """ + + def __init__(self, mapdl): + from ansys.mapdl.core.mapdl import _MapdlCore # lazy import to avoid circular + + if not isinstance(mapdl, _MapdlCore): # pragma: no cover + raise TypeError("Must be implemented from MAPDL class") + + self._mapdl_weakref = weakref.ref(mapdl) + self._stats = None + self._repr_keys = { + "Product": "product", + "MAPDL Version": "mapdl_version", + "PyMAPDL Version": "pymapdl_version", + } + + @property + def _mapdl(self): + """Return the weakly referenced MAPDL instance.""" + return self._mapdl_weakref() + + def _update(self): + """We might need to do more calls if we implement properties + that change over the MAPDL session.""" + try: + if self._mapdl._exited: # pragma: no cover + raise RuntimeError("Information class: MAPDL exited") + + stats = self._mapdl.slashstatus("ALL") + except Exception: # pragma: no cover + self._stats = None + raise RuntimeError("Information class: MAPDL exited") + + stats = stats.replace("\n ", "\n") # Bit of formatting + self._stats = stats + self._mapdl._log.debug("Information class: Updated") + + def __repr__(self): + if not self._stats: # pragma: no cover + self._update() + + return "\n".join( + [ + f"{each_name}:".ljust(25) + f"{getattr(self, each_attr)}".ljust(25) + for each_name, each_attr in self._repr_keys.items() + ] + ) + + @property + @update_information_first(False) + def product(self): + """Retrieve the product from the MAPDL instance.""" + return self._get_product() + + @property + @update_information_first(False) + def mapdl_version(self): + """Retrieve the MAPDL version from the MAPDL instance.""" + return self._get_mapdl_version() + + @property + @update_information_first(False) + def mapdl_version_release(self): + """Retrieve the MAPDL version release from the MAPDL instance.""" + st = self._get_mapdl_version() + return self._get_between("RELEASE", "BUILD", st).strip() + + @property + @update_information_first(False) + def mapdl_version_build(self): + """Retrieve the MAPDL version build from the MAPDL instance.""" + st = self._get_mapdl_version() + return self._get_between("BUILD", "UPDATE", st).strip() + + @property + @update_information_first(False) + def mapdl_version_update(self): + """Retrieve the MAPDL version update from the MAPDL instance.""" + st = self._get_mapdl_version() + return self._get_between("UPDATE", "", st).strip() + + @property + @update_information_first(False) + def pymapdl_version(self): + """Retrieve the PyMAPDL version from the MAPDL instance.""" + return self._get_pymapdl_version() + + @property + @update_information_first(False) + def products(self): + """Retrieve the products from the MAPDL instance.""" + return self._get_products() + + @property + @update_information_first(False) + def preprocessing_capabilities(self): + """Retrieve the preprocessing capabilities from the MAPDL instance.""" + return self._get_preprocessing_capabilities() + + @property + @update_information_first(False) + def aux_capabilities(self): + """Retrieve the aux capabilities from the MAPDL instance.""" + return self._get_aux_capabilities() + + @property + @update_information_first(True) + def solution_options(self): + """Retrieve the solution options from the MAPDL instance.""" + return self._get_solution_options() + + @property + @update_information_first(False) + def post_capabilities(self): + """Retrieve the post capabilities from the MAPDL instance.""" + return self._get_post_capabilities() + + @property + @update_information_first(True) + def titles(self): + """Retrieve the titles from the MAPDL instance.""" + return self._get_titles() + + @property + @update_information_first(True) + def title(self): + """Retrieve and set the title from the MAPDL instance.""" + return self._mapdl.inquire("", "title") + + @title.setter + def title(self, title): + return self._mapdl.title(title) + + @property + @update_information_first(True) + def stitles(self, i=None): + """Retrieve or set the value for the MAPDL stitle (subtitles). + + If 'stitle' includes newline characters (`\\n`), then each line + is assigned to one STITLE. + + If 'stitle' is equals ``None``, the stitles are reset. + + If ``i`` is supplied, only set the stitle number i. + + Starting from 0 up to 3 (Python indexing). + """ + if not i: + return self._get_stitles() + else: + return self._get_stitles()[i] + + @stitles.setter + def stitles(self, stitle, i=None): + if stitle is None: + # Case to empty + stitle = ["", "", "", ""] + + if not isinstance(stitle, (str, list)): + raise ValueError("Only str or list are allowed for stitle") + + if isinstance(stitle, str): + if "\n" in stitle: + stitle = stitle.splitlines() + else: + stitle = "\n".join( + [stitle[ii : ii + 70] for ii in range(0, len(stitle), 70)] + ) + + if any([len(each) > 70 for each in stitle]): + raise ValueError("The number of characters per subtitle is limited to 70.") + + if not i: + for each_index, each_stitle in zip(range(1, 5), stitle): + self._mapdl.stitle(each_index, each_stitle) + else: + self._mapdl.stitle(i, stitle) + + @property + @update_information_first(True) + def units(self): + """Retrieve the units from the MAPDL instance.""" + return self._get_units() + + @property + @update_information_first(True) + def scratch_memory_status(self): + """Retrieve the scratch memory status from the MAPDL instance.""" + return self._get_scratch_memory_status() + + @property + @update_information_first(True) + def database_status(self): + """Retrieve the database status from the MAPDL instance.""" + return self._get_database_status() + + @property + @update_information_first(True) + def config_values(self): + """Retrieve the config values from the MAPDL instance.""" + return self._get_config_values() + + @property + @update_information_first(True) + def global_status(self): + """Retrieve the global status from the MAPDL instance.""" + return self._get_global_status() + + @property + @update_information_first(True) + def job_information(self): + """Retrieve the job information from the MAPDL instance.""" + return self._get_job_information() + + @property + @update_information_first(True) + def model_information(self): + """Retrieve the model information from the MAPDL instance.""" + return self._get_model_information() + + @property + @update_information_first(True) + def boundary_condition_information(self): + """Retrieve the boundary condition information from the MAPDL instance.""" + return self._get_boundary_condition_information() + + @property + @update_information_first(True) + def routine_information(self): + """Retrieve the routine information from the MAPDL instance.""" + return self._get_routine_information() + + @property + @update_information_first(True) + def solution_options_configuration(self): + """Retrieve the solution options configuration from the MAPDL instance.""" + return self._get_solution_options_configuration() + + @property + @update_information_first(True) + def load_step_options(self): + """Retrieve the load step options from the MAPDL instance.""" + return self._get_load_step_options() + + def _get_between(self, init_string, end_string=None, string=None): + if not string: + string = self._stats + + st = string.find(init_string) + len(init_string) + + if not end_string: + en = None + else: + en = string.find(end_string) + return "\n".join(string[st:en].splitlines()).strip() + + def _get_product(self): + return self._get_products().splitlines()[0] + + def _get_mapdl_version(self): + titles_ = self._get_titles() + st = titles_.find("RELEASE") + en = titles_.find("INITIAL", st) + return titles_[st:en].split("CUSTOMER")[0].strip() + + def _get_pymapdl_version(self): + return pymapdl.__version__ + + def _get_title(self): + match = re.match(r"TITLE=(.*)$", self._get_titles()) + if match: + return match.groups(1)[0].strip() + + def _get_stitles(self): + return [ + re.search(f"SUBTITLE {i}=(.*)", self._get_titles()).groups(1)[0].strip() + for i in range(1, 5) + if re.search(f"SUBTITLE {i}=(.*)", self._get_titles()) + ] + + def _get_products(self): + init_ = "*** Products ***" + end_string = "*** PreProcessing Capabilities ***" + return self._get_between(init_, end_string) + + def _get_preprocessing_capabilities(self): + init_ = "*** PreProcessing Capabilities ***" + end_string = "*** Aux Capabilities ***" + return self._get_between(init_, end_string) + + def _get_aux_capabilities(self): + init_ = "*** Aux Capabilities ***" + end_string = "*** Solution Options ***" + return self._get_between(init_, end_string) + + def _get_solution_options(self): + init_ = "*** Solution Options ***" + end_string = "*** Post Capabilities ***" + return self._get_between(init_, end_string) + + def _get_post_capabilities(self): + init_ = "*** Post Capabilities ***" + end_string = "***** TITLES *****" + return self._get_between(init_, end_string) + + def _get_titles(self): + init_ = "***** TITLES *****" + end_string = "***** UNITS *****" + return self._get_between(init_, end_string) + + def _get_units(self): + init_ = "***** UNITS *****" + end_string = "***** SCRATCH MEMORY STATUS *****" + return self._get_between(init_, end_string) + + def _get_scratch_memory_status(self): + init_ = "***** SCRATCH MEMORY STATUS *****" + end_string = "***** DATABASE STATUS *****" + return self._get_between(init_, end_string) + + def _get_database_status(self): + init_ = "***** DATABASE STATUS *****" + end_string = "***** CONFIG VALUES *****" + return self._get_between(init_, end_string) + + def _get_config_values(self): + init_ = "***** CONFIG VALUES *****" + end_string = "G L O B A L S T A T U S" + return self._get_between(init_, end_string) + + def _get_global_status(self): + init_ = "G L O B A L S T A T U S" + end_string = "J O B I N F O R M A T I O N" + return self._get_between(init_, end_string) + + def _get_job_information(self): + init_ = "J O B I N F O R M A T I O N" + end_string = "M O D E L I N F O R M A T I O N" + return self._get_between(init_, end_string) + + def _get_model_information(self): + init_ = "M O D E L I N F O R M A T I O N" + end_string = "B O U N D A R Y C O N D I T I O N I N F O R M A T I O N" + return self._get_between(init_, end_string) + + def _get_boundary_condition_information(self): + init_ = "B O U N D A R Y C O N D I T I O N I N F O R M A T I O N" + end_string = "R O U T I N E I N F O R M A T I O N" + return self._get_between(init_, end_string) + + def _get_routine_information(self): + init_ = "R O U T I N E I N F O R M A T I O N" + end_string = None + return self._get_between(init_, end_string) + + def _get_solution_options_configuration(self): + init_ = "S O L U T I O N O P T I O N S" + end_string = "L O A D S T E P O P T I O N S" + return self._get_between(init_, end_string) + + def _get_load_step_options(self): + init_ = "L O A D S T E P O P T I O N S" + end_string = None + return self._get_between(init_, end_string) diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index b61cd35b8b9..0718475d85c 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -1363,6 +1363,13 @@ def test_mpfunctions(mapdl, cube_solve, capsys): mapdl.mpwrite("/test_dir/test", "mp") +def test_mapdl_str(mapdl): + out = str(mapdl) + assert "ansys" in out.lower() + assert "Product" in out + assert "MAPDL Version" in out + + def test_plot_empty_mesh(mapdl, cleared): with pytest.warns(UserWarning): mapdl.nplot(vtk=True) diff --git a/tests/test_misc.py b/tests/test_misc.py index afde00be567..c8226538358 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,4 +1,6 @@ """Small or misc tests that don't fit in other test modules""" +import inspect + import pytest from pyvista.plotting import system_supports_plotting @@ -68,3 +70,45 @@ def test_check_valid_start_instance(start_instance): def test_get_ansys_bin(mapdl): rver = mapdl.__str__().splitlines()[1].split(":")[1].strip().replace(".", "") assert isinstance(get_ansys_bin(rver), str) + + +def test_mapdl_info(mapdl, capfd): + info = mapdl.info + for attr, value in inspect.getmembers(info): + if not attr.startswith("_") and attr not in ["title", "stitles"]: + assert isinstance(value, str) + + with pytest.raises(AttributeError): + setattr(info, attr, "any_value") + + assert "PyMAPDL" in mapdl.info.__repr__() + out = info.__str__() + + assert "ansys" in out.lower() + assert "Product" in out + assert "MAPDL Version" in out + assert "UPDATE" in out + + +def test_info_title(mapdl): + title = "this is my title" + mapdl.info.title = title + assert title == mapdl.info.title + + +def test_info_stitle(mapdl): + info = mapdl.info + + assert not info.stitles + stitles = ["asfd", "qwer", "zxcv", "jkl"] + info.stitles = "\n".join(stitles) + + assert stitles == info.stitles + + stitles = stitles[::-1] + + info.stitles = stitles + assert stitles == info.stitles + + info.stitles = None + assert not info.stitles